diff --git a/lib/minify/README.txt b/lib/minify/README.txt deleted file mode 100644 index 6f527a5f69f..00000000000 --- a/lib/minify/README.txt +++ /dev/null @@ -1,132 +0,0 @@ -The files in this directory represent the default Minify setup designed to ease -integration with your site. This app will combine and minify your Javascript or -CSS files and serve them with HTTP compression and cache headers. - - -RECOMMENDED - -It's recommended to edit config.php to set $min_cachePath to a writeable -(by PHP) directory on your system. This will improve performance. - - -GETTING STARTED - -The quickest way to get started is to use the Minify URI Builder application -on your website: http://example.com/min/builder/ - - -MINIFYING A SINGLE FILE - -Let's say you want to serve this file: - http://example.com/wp-content/themes/default/default.css - -Here's the "Minify URL" for this file: - http://example.com/min/?f=wp-content/themes/default/default.css - -In other words, the "f" argument is set to the file path from root without the -initial "/". As CSS files may contain relative URIs, Minify will automatically -"fix" these by rewriting them as root relative. - - -COMBINING MULTIPLE FILES IN ONE DOWNLOAD - -Separate the paths given to "f" with commas. - -Let's say you have CSS files at these URLs: - http://example.com/scripts/jquery-1.2.6.js - http://example.com/scripts/site.js - -You can combine these files through Minify by requesting this URL: - http://example.com/min/?f=scripts/jquery-1.2.6.js,scripts/site.js - - -SIMPLIFYING URLS WITH A BASE PATH - -If you're combining files that share the same ancestor directory, you can use -the "b" argument to set the base directory for the "f" argument. Do not include -the leading or trailing "/" characters. - -E.g., the following URLs will serve the exact same content: - http://example.com/min/?f=scripts/jquery-1.2.6.js,scripts/site.js,scripts/home.js - http://example.com/min/?b=scripts&f=jquery-1.2.6.js,site.js,home.js - - -MINIFY URLS IN HTML - -In (X)HTML files, don't forget to replace any "&" characters with "&". - - -SPECIFYING ALLOWED DIRECTORIES - -By default, Minify will serve any *.css/*.js files within the DOCUMENT_ROOT. If -you'd prefer to limit Minify's access to certain directories, set the -$min_serveOptions['minApp']['allowDirs'] array in config.php. E.g. to limit -to the /js and /themes/default directories, use: - -$min_serveOptions['minApp']['allowDirs'] = array('//js', '//themes/default'); - - -GROUPS: FASTER PERFORMANCE AND BETTER URLS - -For the best performance, edit groupsConfig.php to pre-specify groups of files -to be combined under preset keys. E.g., here's an example configuration in -groupsConfig.php: - -return array( - 'js' => array('//js/Class.js', '//js/email.js') -); - -This pre-selects the following files to be combined under the key "js": - http://example.com/js/Class.js - http://example.com/js/email.js - -You can now serve these files with this simple URL: - http://example.com/min/?g=js - - -GROUPS: SPECIFYING FILES OUTSIDE THE DOC_ROOT - -In the groupsConfig.php array, the "//" in the file paths is a shortcut for -the DOCUMENT_ROOT, but you can also specify paths from the root of the filesystem -or relative to the DOC_ROOT: - -return array( - 'js' => array( - '//js/file.js' // file within DOC_ROOT - ,'//../file.js' // file in parent directory of DOC_ROOT - ,'C:/Users/Steve/file.js' // file anywhere on filesystem - ) -); - - -FAR-FUTURE EXPIRES HEADERS - -Minify can send far-future (one year) Expires headers. To enable this you must -add a number to the querystring (e.g. /min/?g=js&1234 or /min/f=file.js&1234) -and alter it whenever a source file is changed. If you have a build process you -can use a build/source control revision number. - -If you serve files as a group, you can use the utility function Minify_groupUri() -to get a "versioned" Minify URI for use in your HTML. E.g.: - -"; - - -DEBUG MODE - -In debug mode, instead of compressing files, Minify sends combined files with -comments prepended to each line to show the line number in the original source -file. To enable this, set $min_allowDebugFlag to true in config.php and append -"&debug=1" to your URIs. E.g. /min/?f=script1.js,script2.js&debug=1 - -Known issue: files with comment-like strings/regexps can cause problems in this mode. - - -QUESTIONS? - -http://groups.google.com/group/minify diff --git a/lib/minify/builder/_index.js b/lib/minify/builder/_index.js deleted file mode 100644 index c4b665546e8..00000000000 --- a/lib/minify/builder/_index.js +++ /dev/null @@ -1,242 +0,0 @@ -var MUB = { - _uid : 0 - ,_minRoot : '/min/?' - ,checkRewrite : function () { - var testUri = location.pathname.replace(/\/[^\/]*$/, '/rewriteTest.js').substr(1); - function fail() { - $('#minRewriteFailed')[0].className = 'topNote'; - }; - $.ajax({ - url : '../f=' + testUri + '&' + (new Date()).getTime() - ,success : function (data) { - if (data === '1') { - MUB._minRoot = '/min/'; - $('span.minRoot').html('/min/'); - } else - fail(); - } - ,error : fail - }); - } - /** - * Get markup for new source LI element - */ - ,newLi : function () { - return '
  • http://' + location.host + '/' - + ' ' - + '
  • '; - } - /** - * Add new empty source LI and attach handlers to buttons - */ - ,addLi : function () { - $('#sources').append(MUB.newLi()); - var li = $('#li' + MUB._uid)[0]; - $('button[title=Remove]', li).click(function () { - $('#results').hide(); - var hadValue = !!$('input', li)[0].value; - $(li).remove(); - }); - $('button[title$=Earlier]', li).click(function () { - $(li).prev('li').find('input').each(function () { - $('#results').hide(); - // this = previous li input - var tmp = this.value; - this.value = $('input', li).val(); - $('input', li).val(tmp); - MUB.updateAllTestLinks(); - }); - }); - $('button[title$=Later]', li).click(function () { - $(li).next('li').find('input').each(function () { - $('#results').hide(); - // this = next li input - var tmp = this.value; - this.value = $('input', li).val(); - $('input', li).val(tmp); - MUB.updateAllTestLinks(); - }); - }); - ++MUB._uid; - } - /** - * In the context of a source LI element, this will analyze the URI in - * the INPUT and check the URL on the site. - */ - ,liUpdateTestLink : function () { // call in context of li element - if (! $('input', this)[0].value) - return; - var li = this; - $('span', this).html(''); - var url = 'http://' + location.host + '/' - + $('input', this)[0].value.replace(/^\//, ''); - $.ajax({ - url : url - ,complete : function (xhr, stat) { - if ('success' == stat) - $('span', li).html('✓'); - else { - $('span', li).html('') - .find('button').click(function () { - MUB.liUpdateTestLink.call(li); - }); - } - } - ,dataType : 'text' - }); - } - /** - * Check all source URLs - */ - ,updateAllTestLinks : function () { - $('#sources li').each(MUB.liUpdateTestLink); - } - /** - * In a given array of strings, find the character they all have at - * a particular index - * @param Array arr array of strings - * @param Number pos index to check - * @return mixed a common char or '' if any do not match - */ - ,getCommonCharAtPos : function (arr, pos) { - var i - ,l = arr.length - ,c = arr[0].charAt(pos); - if (c === '' || l === 1) - return c; - for (i = 1; i < l; ++i) - if (arr[i].charAt(pos) !== c) - return ''; - return c; - } - /** - * Get the shortest URI to minify the set of source files - * @param Array sources URIs - */ - ,getBestUri : function (sources) { - var pos = 0 - ,base = '' - ,c; - while (true) { - c = MUB.getCommonCharAtPos(sources, pos); - if (c === '') - break; - else - base += c; - ++pos; - } - base = base.replace(/[^\/]+$/, ''); - var uri = MUB._minRoot + 'f=' + sources.join(','); - if (base.charAt(base.length - 1) === '/') { - // we have a base dir! - var basedSources = sources - ,i - ,l = sources.length; - for (i = 0; i < l; ++i) { - basedSources[i] = sources[i].substr(base.length); - } - base = base.substr(0, base.length - 1); - var bUri = MUB._minRoot + 'b=' + base + '&f=' + basedSources.join(','); - //window.console && console.log([uri, bUri]); - uri = uri.length < bUri.length - ? uri - : bUri; - } - return uri; - } - /** - * Create the Minify URI for the sources - */ - ,update : function () { - MUB.updateAllTestLinks(); - var sources = [] - ,ext = false - ,fail = false; - $('#sources input').each(function () { - var m, val; - if (! fail && this.value && (m = this.value.match(/\.(css|js)$/))) { - var thisExt = m[1]; - if (ext === false) - ext = thisExt; - else if (thisExt !== ext) { - fail = true; - return alert('extensions must match!'); - } - this.value = this.value.replace(/^\//, ''); - if (-1 != $.inArray(this.value, sources)) { - fail = true; - return alert('duplicate file!'); - } - sources.push(this.value); - } - }); - if (fail || ! sources.length) - return; - $('#groupConfig').val(" 'keyName' => array('//" + sources.join("', '//") + "'),"); - var uri = MUB.getBestUri(sources) - ,uriH = uri.replace(//, '>').replace(/&/, '&'); - $('#uriA').html(uriH)[0].href = uri; - $('#uriHtml').val( - ext === 'js' - ? '' - : '' - ); - $('#results').show(); - } - /** - * Handler for the "Add file +" button - */ - ,addButtonClick : function () { - $('#results').hide(); - MUB.addLi(); - MUB.updateAllTestLinks(); - $('#update').show().click(MUB.update); - $('#sources li:last input')[0].focus(); - } - /** - * Runs on DOMready - */ - ,init : function () { - $('#app').show(); - $('#sources').html(''); - $('#add button').click(MUB.addButtonClick); - // make easier to copy text out of - $('#uriHtml, #groupConfig').click(function () { - this.select(); - }).focus(function () { - this.select(); - }); - $('a.ext').attr({target:'_blank'}); - if (location.hash) { - // make links out of URIs from bookmarklet - $('#getBm').hide(); - $('#bmUris').html('

    Found by bookmarklet: /' - + location.hash.substr(1).split(',').join(' | /') - + '

    ' - ); - $('#bmUris a').click(function () { - MUB.addButtonClick(); - $('#sources li:last input').val(this.innerHTML) - MUB.liUpdateTestLink.call($('#sources li:last')[0]); - $('#results').hide(); - return false; - }).attr({title:'Add file +'}); - } else { - // copy bookmarklet code into href - var bmUri = location.pathname.replace(/\/[^\/]*$/, '/bm.js').substr(1); - $.ajax({ - url : '../?f=' + bmUri - ,success : function (code) { - $('#bm')[0].href = code - .replace('%BUILDER_URL%', location.href) - .replace(/\n/g, ' '); - } - ,dataType : 'text' - }); - $.browser.msie && $('#getBm p:last').append(' Sorry, not supported in MSIE!'); - MUB.addButtonClick(); - } - MUB.checkRewrite(); - } -}; -window.onload = MUB.init; diff --git a/lib/minify/builder/bm.js b/lib/minify/builder/bm.js deleted file mode 100644 index 703039ba724..00000000000 --- a/lib/minify/builder/bm.js +++ /dev/null @@ -1,36 +0,0 @@ -javascript:(function() { - var d = document - ,uris = [] - ,i = 0 - ,o - ,home = (location + '').split('/').splice(0, 3).join('/') + '/'; - function add(uri) { - return (0 === uri.indexOf(home)) - && (!/[\?&]/.test(uri)) - && uris.push(escape(uri.substr(home.length))); - }; - function sheet(ss) { - // we must check the domain with add() before accessing ss.cssRules - // otherwise a security exception will be thrown - if (ss.href && add(ss.href) && ss.cssRules) { - var i = 0, r; - while (r = ss.cssRules[i++]) - r.styleSheet && sheet(r.styleSheet); - } - }; - while (o = d.getElementsByTagName('script')[i++]) - o.src && !(o.type && /vbs/i.test(o.type)) && add(o.src); - i = 0; - while (o = d.styleSheets[i++]) - /* http://www.w3.org/TR/DOM-Level-2-Style/stylesheets.html#StyleSheets-DocumentStyle-styleSheets - document.styleSheet is a list property where [0] accesses the 1st element and - [outOfRange] returns null. In IE, styleSheets is a function, and also throws an - exception when you check the out of bounds index. (sigh) */ - sheet(o); - if (uris.length) - window.open('%BUILDER_URL%#' + uris.join(',')); - else - alert('No js/css files found with URLs within "' - + home.split('/')[2] - + '".\n(This tool is limited to URLs with the same domain.)'); -})(); diff --git a/lib/minify/builder/index.php b/lib/minify/builder/index.php deleted file mode 100644 index 1b209822202..00000000000 --- a/lib/minify/builder/index.php +++ /dev/null @@ -1,182 +0,0 @@ - - - - - Minify URI Builder - - - - -

    Note: Please set $min_cachePath -in /min/config.php to improve performance.

    - - -

    Note: Your webserver does not seem to - support mod_rewrite (used in /min/.htaccess). Your Minify URIs will contain "?", which -may reduce the benefit of proxy cache servers.

    - -

    Minify URI Builder

    - - - -
    - -

    Create a list of Javascript or CSS files (or 1 is fine) you'd like to combine -and click [Update].

    - -
    -
    - -
    - -

    - -
    - -

    Minify URI

    -

    Place this URI in your HTML to serve the files above combined, minified, compressed and -with cache headers.

    - - - -
    URI/min (opens in new window)
    HTML
    - -

    How to serve these files as a group

    -

    For the best performance you can serve these files as a pre-defined group with a URI -like: /min/?g=keyName

    -

    To do this, add a line like this to /min/groupsConfig.php:

    - -
    return array(
    -    ... your existing groups here ...
    -
    -);
    - -

    Make sure to replace keyName with a unique key for this group.

    -
    - -
    -

    Find URIs on a Page

    -

    You can use the bookmarklet below to fetch all CSS & Javascript URIs from a page -on your site. When you active it, this page will open in a new window with a list of -available URIs to add.

    - -

    Create Minify URIs (right-click, add to bookmarks)

    -
    - -

    Combining CSS files that contain @import

    -

    If your CSS files contain @import declarations, Minify will not -remove them. Therefore, you will want to remove those that point to files already -in your list, and move any others to the top of the first file in your list -(imports below any styles will be ignored by browsers as invalid).

    -

    If you desire, you can use Minify URIs in imports and they will not be touched -by Minify. E.g. @import "/min/?g=css2";

    - -
    - -
    -

    Need help? Search or post to the Minify discussion list.

    -

    This app is minified :) view -source

    - - - - - - - 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'; - -if (0 === stripos(PHP_OS, 'win')) { - Minify::setDocRoot(); // we may be on IIS -} -Minify::setCache(isset($min_cachePath) ? $min_cachePath : null); -Minify::$uploaderHoursBehind = $min_uploaderHoursBehind; - -Minify::serve('Page', $serveOpts); diff --git a/lib/minify/builder/ocCheck.php b/lib/minify/builder/ocCheck.php deleted file mode 100644 index 8cc19af8b2a..00000000000 --- a/lib/minify/builder/ocCheck.php +++ /dev/null @@ -1,36 +0,0 @@ - 'World!' - ,'method' => 'deflate' - )); - $he->encode(); - $he->sendAll(); - -} else { - // echo status "0" or "1" - header('Content-Type: text/plain'); - echo (int)$_oc; -} diff --git a/lib/minify/builder/rewriteTest.js b/lib/minify/builder/rewriteTest.js deleted file mode 100644 index d00491fd7e5..00000000000 --- a/lib/minify/builder/rewriteTest.js +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/lib/minify/config.php b/lib/minify/config.php index 7e29c0b26e7..e2f709cea9f 100644 --- a/lib/minify/config.php +++ b/lib/minify/config.php @@ -1,19 +1,40 @@ tempdir; +$min_documentRoot = $CFG->dirroot.'/lib/minify'; +$min_cacheFileLocking = true; +$min_serveOptions['bubbleCssImports'] = false; +$min_serveOptions['maxAge'] = 1800; +$min_serveOptions['minApp']['groupsOnly'] = true; +$min_symlinks = array(); +$min_uploaderHoursBehind = 0; +$min_libPath = dirname(__FILE__) . '/lib'; +// do not change zlib compression or buffering here + +// TODO: locking setting, caching setting + +return; // end of moodle modification + /** - * In 'debug' mode, Minify can combine files with no minification and - * add comments to indicate line #s of the original files. - * - * To allow debugging, set this option to true and add "&debug=1" to - * a URI. E.g. /min/?f=script1.js,script2.js&debug=1 - */ -$min_allowDebugFlag = ($CFG->debug); + * Allow use of the Minify URI Builder app. Only set this to true while you need it. + **/ +$min_enableBuilder = true; /** @@ -23,24 +44,38 @@ $min_allowDebugFlag = ($CFG->debug); * * If you want to use a custom error logger, set this to your logger * instance. Your object should have a method log(string $message). - * - * @todo cache system does not have error logging yet. */ $min_errorLogger = false; /** - * Allow use of the Minify URI Builder app. If you no longer need - * this, set to false. - **/ -$min_enableBuilder = false; + * To allow debug mode output, you must set this option to true. + * + * Once true, you can send the cookie minDebug to request debug mode output. The + * cookie value should match the URIs you'd like to debug. E.g. to debug + * /min/f=file1.js send the cookie minDebug=file1.js + * You can manually enable debugging by appending "&debug" to a URI. + * E.g. /min/?f=script1.js,script2.js&debug + * + * In 'debug' mode, Minify combines files with no minification and adds comments + * to indicate line #s of the original files. + */ +$min_allowDebugFlag = false; /** * For best performance, specify your temp directory here. Otherwise Minify * will have to load extra code to guess. Some examples below: */ -$min_cachePath = $CFG->tempdir.''; +//$min_cachePath = 'c:\\WINDOWS\\Temp'; +//$min_cachePath = '/tmp'; +//$min_cachePath = preg_replace('/^\\d+;/', '', session_save_path()); +/** + * To use APC/Memcache/ZendPlatform for cache storage, require the class and + * set $min_cachePath to an instance. Example below: + */ +//require dirname(__FILE__) . '/lib/Minify/Cache/APC.php'; +//$min_cachePath = new Minify_Cache_APC(); /** @@ -53,8 +88,8 @@ $min_cachePath = $CFG->tempdir.''; * If /min/ is directly inside your document root, just uncomment the * second line. The third line might work on some Apache servers. */ -$min_documentRoot = $CFG->dirroot.'/lib/minify'; -//$min_documentRoot = substr(__FILE__, 0, strlen(__FILE__) - 15); +$min_documentRoot = ''; +//$min_documentRoot = substr(__FILE__, 0, -15); //$min_documentRoot = $_SERVER['SUBDOMAIN_DOCUMENT_ROOT']; @@ -77,9 +112,9 @@ $min_serveOptions['bubbleCssImports'] = false; /** - * Maximum age of browser cache in seconds. After this period, the browser - * will send another conditional GET. Use a longer period for lower traffic - * but you may want to shorten this before making changes if it's crucial + * Cache-Control: max-age value sent to browser (in seconds). After this period, + * the browser will send another conditional GET. Use a longer period for lower + * traffic but you may want to shorten this before making changes if it's crucial * those changes are seen immediately. * * Note: Despite this setting, if you include a number at the end of the @@ -88,6 +123,18 @@ $min_serveOptions['bubbleCssImports'] = false; $min_serveOptions['maxAge'] = 1800; +/** + * To use Google's Closure Compiler API (falling back to JSMin on failure), + * uncomment the following lines: + */ +/*function closureCompiler($js) { + require_once 'Minify/JS/ClosureCompiler.php'; + return Minify_JS_ClosureCompiler::minify($js); +} +$min_serveOptions['minifiers']['application/x-javascript'] = 'closureCompiler'; +//*/ + + /** * If you'd like to restrict the "f" option to files within/below * particular directories below DOCUMENT_ROOT, set this here. @@ -102,12 +149,17 @@ $min_serveOptions['maxAge'] = 1800; * Set to true to disable the "f" GET parameter for specifying files. * Only the "g" parameter will be considered. */ -$min_serveOptions['minApp']['groupsOnly'] = true; +$min_serveOptions['minApp']['groupsOnly'] = false; + /** - * Maximum # of files that can be specified in the "f" GET parameter + * By default, Minify will not minify files with names containing .min or -min + * before the extension. E.g. myFile.min.js will not be processed by JSMin + * + * To minify all files, set this option to null. You could also specify your + * own pattern that is matched against the filename. */ -$min_serveOptions['minApp']['maxFiles'] = 10; +//$min_serveOptions['minApp']['noMinPattern'] = '@[-\\.]min\\.(?:js|css)$@i'; /** @@ -148,7 +200,7 @@ $min_uploaderHoursBehind = 0; * Path to Minify's lib folder. If you happen to move it, change * this accordingly. */ -$min_libPath = $CFG->libdir . '/minify/lib'; +$min_libPath = dirname(__FILE__) . '/lib'; // try to disable output_compression (may not have an effect) diff --git a/lib/minify/groupsConfig.php b/lib/minify/groupsConfig.php index 56398807283..c9007768381 100644 --- a/lib/minify/groupsConfig.php +++ b/lib/minify/groupsConfig.php @@ -7,28 +7,11 @@ /** * You may wish to use the Minify URI Builder app to suggest * changes. http://yourdomain/min/builder/ + * + * See http://code.google.com/p/minify/wiki/CustomSource for other ideas **/ return array( // 'js' => 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/lib/minify/lib/FirePHP.php b/lib/minify/lib/FirePHP.php index d9d12106c82..d301a641abf 100644 --- a/lib/minify/lib/FirePHP.php +++ b/lib/minify/lib/FirePHP.php @@ -1065,7 +1065,7 @@ class FirePHP { * @author Brett Stimmerman * @author Christoph Dorn * @copyright 2005 Michal Migurski - * @version CVS: $Id$ + * @version CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $ * @license http://www.opensource.org/licenses/bsd-license.php * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 */ diff --git a/lib/minify/lib/HTTP/ConditionalGet.php b/lib/minify/lib/HTTP/ConditionalGet.php index 823db058fa6..93b7e75d8e1 100644 --- a/lib/minify/lib/HTTP/ConditionalGet.php +++ b/lib/minify/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. @@ -106,8 +105,6 @@ class HTTP_ConditionalGet { * seconds, and also set the Expires header to the equivalent GMT date. * After the max-age period has passed, the browser will again send a * conditional GET to revalidate its cache. - * - * @return null */ public function __construct($spec) { @@ -150,7 +147,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 +209,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) { @@ -230,8 +232,6 @@ class HTTP_ConditionalGet { * "private" will be sent, allowing only browser caching. * * @param array $options (default empty) additional options for constructor - * - * @return null */ public static function check($lastModifiedTime = null, $isPublic = false, $options = array()) { @@ -267,13 +267,21 @@ class HTTP_ConditionalGet { protected $_lmTime = null; protected $_etag = null; protected $_stripEtag = false; - + + /** + * @param string $hash + * + * @param string $scope + */ protected function _setEtag($hash, $scope) { $this->_etag = '"' . substr($scope, 0, 3) . $hash . '"'; $this->_headers['ETag'] = $this->_etag; } + /** + * @param int $time + */ protected function _setLastModified($time) { $this->_lmTime = (int)$time; @@ -282,6 +290,8 @@ class HTTP_ConditionalGet { /** * Determine validity of client cache and queue 304 header if valid + * + * @return bool */ protected function _isCacheValid() { @@ -298,6 +308,9 @@ class HTTP_ConditionalGet { return $isValid; } + /** + * @return bool + */ protected function resourceMatchedEtag() { if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) { @@ -319,7 +332,12 @@ class HTTP_ConditionalGet { } return false; } - + + /** + * @param string $etag + * + * @return string + */ protected function normalizeEtag($etag) { $etag = trim($etag); return $this->_stripEtag @@ -327,17 +345,17 @@ class HTTP_ConditionalGet { : $etag; } + /** + * @return bool + */ protected function resourceNotModified() { 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/lib/minify/lib/HTTP/Encoder.php b/lib/minify/lib/HTTP/Encoder.php index 05ca552ecb6..8f347793cfa 100644 --- a/lib/minify/lib/HTTP/Encoder.php +++ b/lib/minify/lib/HTTP/Encoder.php @@ -59,7 +59,7 @@ class HTTP_Encoder { * * @var bool */ - public static $encodeToIe6 = false; + public static $encodeToIe6 = true; /** @@ -85,13 +85,16 @@ class HTTP_Encoder { * method. If not set, the best method will be chosen by getAcceptedEncoding() * The available methods are 'gzip', 'deflate', 'compress', and '' (no * encoding) - * - * @return null */ 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']; } @@ -109,7 +112,7 @@ class HTTP_Encoder { * * Call after encode() for encoded content. * - * return string + * @return string */ public function getContent() { @@ -143,8 +146,6 @@ class HTTP_Encoder { * not handled purposefully. * * @see getHeaders() - * - * @return null */ public function sendHeaders() { @@ -161,8 +162,6 @@ class HTTP_Encoder { * You must call this before headers are sent and it probably cannot be * used in conjunction with zlib output buffering / mod_gzip. Errors are * not handled purposefully. - * - * @return null */ public function sendAll() { @@ -195,7 +194,7 @@ class HTTP_Encoder { // @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html if (! isset($_SERVER['HTTP_ACCEPT_ENCODING']) - || self::_isBuggyIe()) + || self::isBuggyIe()) { return array('', ''); } @@ -253,7 +252,9 @@ class HTTP_Encoder { */ public function encode($compressionLevel = null) { - $this->_headers['Vary'] = 'Accept-Encoding'; + if (! self::isBuggyIe()) { + $this->_headers['Vary'] = 'Accept-Encoding'; + } if (null === $compressionLevel) { $compressionLevel = self::$compressionLevel; } @@ -273,7 +274,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; @@ -301,16 +304,17 @@ class HTTP_Encoder { $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() { + if (empty($_SERVER['HTTP_USER_AGENT'])) { + return false; + } $ua = $_SERVER['HTTP_USER_AGENT']; // quick escape for non-IEs if (0 !== strpos($ua, 'Mozilla/4.0 (compatible; MSIE ') @@ -318,9 +322,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/lib/minify/lib/JSMin.php b/lib/minify/lib/JSMin.php index 770e1c61048..b6879f3716a 100644 --- a/lib/minify/lib/JSMin.php +++ b/lib/minify/lib/JSMin.php @@ -1,16 +1,20 @@ + * $minifiedJs = JSMin::minify($js); + * + * + * This is a modified port of jsmin.c. Improvements: * - * 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. + * Does not choke on some regexp literals containing quote characters. E.g. /'/ + * + * Spaces are preserved after some add/sub operators, so they are not mistakenly + * converted to post-inc/dec. E.g. a + ++b -> a+ ++b * + * Preserves multi-line comments that begin with /*! + * * PHP 5 or higher is required. * * Permission is hereby granted to use this version of the library under the @@ -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,11 +68,13 @@ class JSMin { protected $inputLength = 0; protected $lookAhead = null; protected $output = ''; - + protected $lastByteOut = ''; + /** - * Minify Javascript + * Minify Javascript. * * @param string $js Javascript to be minified + * * @return string */ public static function minify($js) @@ -76,38 +82,55 @@ 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; } - + /** * Perform minification, return result + * + * @return string */ public function min() { 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 if ($this->a === ' ') { - if (! $this->isAlphaNum($this->b)) { + if (($this->lastByteOut === '+' || $this->lastByteOut === '-') + && ($this->b === $this->lastByteOut)) { + // Don't delete this space. If we do, the addition/subtraction + // could be parsed as a post-increment + } elseif (! $this->isAlphaNum($this->b)) { $command = self::ACTION_DELETE_A; } } 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)) { @@ -120,19 +143,38 @@ 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. * ACTION_DELETE_A_B = Get the next B. + * + * @param int $command + * @throws JSMin_UnterminatedRegExpException|JSMin_UnterminatedStringException */ protected function action($command) { + if ($command === self::ACTION_DELETE_A_B + && $this->b === ' ' + && ($this->a === '+' || $this->a === '-')) { + // Note: we're at an addition/substraction operator; the inputIndex + // will certainly be a valid index + if ($this->input[$this->inputIndex] === $this->a) { + // This is "+ +" or "- -". Don't delete the space. + $command = self::ACTION_KEEP_A; + } + } switch ($command) { case self::ACTION_KEEP_A: $this->output .= $this->a; + $this->lastByteOut = $this->a; + // fallthrough case self::ACTION_DELETE_A: $this->a = $this->b; @@ -140,17 +182,22 @@ class JSMin { $str = $this->a; // in case needed for exception while (true) { $this->output .= $this->a; + $this->lastByteOut = $this->a; + $this->a = $this->get(); if ($this->a === $this->b) { // end quote break; } 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 === '\\') { $this->output .= $this->a; + $this->lastByteOut = $this->a; + $this->a = $this->get(); $str .= $this->a; } @@ -173,16 +220,21 @@ 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; + $this->lastByteOut = $this->a; } $this->b = $this->next(); } // end case ACTION_DELETE_A_B } } - + + /** + * @return bool + */ protected function isRegexpLiteral() { if (false !== strpos("\n{;(,=:[!&|?", $this->a)) { // we aren't dividing @@ -207,9 +259,11 @@ class JSMin { } return false; } - + /** * Get next char. Convert ctrl char to space. + * + * @return string */ protected function get() { @@ -231,24 +285,33 @@ class JSMin { } return $c; } - + /** * Get next char. If is ctrl character, translate to a space or newline. + * + * @return string */ protected function peek() { $this->lookAhead = $this->get(); return $this->lookAhead; } - + /** * Is $c a letter, digit, underscore, dollar sign, escape, or non-ASCII? + * + * @param string $c + * + * @return bool */ protected function isAlphaNum($c) { return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126); } - + + /** + * @return string + */ protected function singleLineComment() { $comment = ''; @@ -264,7 +327,11 @@ class JSMin { } } } - + + /** + * @return string + * @throws JSMin_UnterminatedCommentException + */ protected function multipleLineComment() { $this->get(); @@ -276,7 +343,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,15 +352,19 @@ 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. + * + * @return string */ protected function next() { diff --git a/lib/minify/lib/JSMinPlus.php b/lib/minify/lib/JSMinPlus.php index 08de8805b70..5a3c5bdff4f 100644 --- a/lib/minify/lib/JSMinPlus.php +++ b/lib/minify/lib/JSMinPlus.php @@ -1,7 +1,7 @@ - * PHP port, modifications and minifier routine are (C) 2009 + * PHP port, modifications and minifier routine are (C) 2009-2011 * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -65,7 +70,8 @@ define('TOKEN_IDENTIFIER', 3); define('TOKEN_STRING', 4); define('TOKEN_REGEXP', 5); define('TOKEN_NEWLINE', 6); -define('TOKEN_CONDCOMMENT_MULTILINE', 7); +define('TOKEN_CONDCOMMENT_START', 7); +define('TOKEN_CONDCOMMENT_END', 8); define('JS_SCRIPT', 100); define('JS_BLOCK', 101); @@ -82,10 +88,89 @@ define('JS_SETTER', 111); define('JS_GROUP', 112); define('JS_LIST', 113); +define('JS_MINIFIED', 999); + define('DECLARED_FORM', 0); define('EXPRESSED_FORM', 1); define('STATEMENT_FORM', 2); +/* Operators */ +define('OP_SEMICOLON', ';'); +define('OP_COMMA', ','); +define('OP_HOOK', '?'); +define('OP_COLON', ':'); +define('OP_OR', '||'); +define('OP_AND', '&&'); +define('OP_BITWISE_OR', '|'); +define('OP_BITWISE_XOR', '^'); +define('OP_BITWISE_AND', '&'); +define('OP_STRICT_EQ', '==='); +define('OP_EQ', '=='); +define('OP_ASSIGN', '='); +define('OP_STRICT_NE', '!=='); +define('OP_NE', '!='); +define('OP_LSH', '<<'); +define('OP_LE', '<='); +define('OP_LT', '<'); +define('OP_URSH', '>>>'); +define('OP_RSH', '>>'); +define('OP_GE', '>='); +define('OP_GT', '>'); +define('OP_INCREMENT', '++'); +define('OP_DECREMENT', '--'); +define('OP_PLUS', '+'); +define('OP_MINUS', '-'); +define('OP_MUL', '*'); +define('OP_DIV', '/'); +define('OP_MOD', '%'); +define('OP_NOT', '!'); +define('OP_BITWISE_NOT', '~'); +define('OP_DOT', '.'); +define('OP_LEFT_BRACKET', '['); +define('OP_RIGHT_BRACKET', ']'); +define('OP_LEFT_CURLY', '{'); +define('OP_RIGHT_CURLY', '}'); +define('OP_LEFT_PAREN', '('); +define('OP_RIGHT_PAREN', ')'); +define('OP_CONDCOMMENT_END', '@*/'); + +define('OP_UNARY_PLUS', 'U+'); +define('OP_UNARY_MINUS', 'U-'); + +/* Keywords */ +define('KEYWORD_BREAK', 'break'); +define('KEYWORD_CASE', 'case'); +define('KEYWORD_CATCH', 'catch'); +define('KEYWORD_CONST', 'const'); +define('KEYWORD_CONTINUE', 'continue'); +define('KEYWORD_DEBUGGER', 'debugger'); +define('KEYWORD_DEFAULT', 'default'); +define('KEYWORD_DELETE', 'delete'); +define('KEYWORD_DO', 'do'); +define('KEYWORD_ELSE', 'else'); +define('KEYWORD_ENUM', 'enum'); +define('KEYWORD_FALSE', 'false'); +define('KEYWORD_FINALLY', 'finally'); +define('KEYWORD_FOR', 'for'); +define('KEYWORD_FUNCTION', 'function'); +define('KEYWORD_IF', 'if'); +define('KEYWORD_IN', 'in'); +define('KEYWORD_INSTANCEOF', 'instanceof'); +define('KEYWORD_NEW', 'new'); +define('KEYWORD_NULL', 'null'); +define('KEYWORD_RETURN', 'return'); +define('KEYWORD_SWITCH', 'switch'); +define('KEYWORD_THIS', 'this'); +define('KEYWORD_THROW', 'throw'); +define('KEYWORD_TRUE', 'true'); +define('KEYWORD_TRY', 'try'); +define('KEYWORD_TYPEOF', 'typeof'); +define('KEYWORD_VAR', 'var'); +define('KEYWORD_VOID', 'void'); +define('KEYWORD_WHILE', 'while'); +define('KEYWORD_WITH', 'with'); + + class JSMinPlus { private $parser; @@ -107,7 +192,7 @@ class JSMinPlus private function __construct() { - $this->parser = new JSParser(); + $this->parser = new JSParser($this); } public static function minify($js, $filename='') @@ -136,42 +221,55 @@ class JSMinPlus return false; } - private function parseTree($n, $noBlockGrouping = false) + public function parseTree($n, $noBlockGrouping = false) { $s = ''; switch ($n->type) { - case KEYWORD_FUNCTION: - $s .= 'function' . ($n->name ? ' ' . $n->name : '') . '('; - $params = $n->params; - for ($i = 0, $j = count($params); $i < $j; $i++) - $s .= ($i ? ',' : '') . $params[$i]; - $s .= '){' . $this->parseTree($n->body, true) . '}'; + case JS_MINIFIED: + $s = $n->value; break; case JS_SCRIPT: - // we do nothing with funDecls or varDecls + // we do nothing yet with funDecls or varDecls $noBlockGrouping = true; - // fall through + // FALL THROUGH + case JS_BLOCK: $childs = $n->treeNodes; + $lastType = 0; for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++) { + $type = $childs[$i]->type; $t = $this->parseTree($childs[$i]); if (strlen($t)) { if ($c) { - if ($childs[$i]->type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM) - $s .= "\n"; // put declared functions on a new line + $s = rtrim($s, ';'); + + if ($type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM) + { + // put declared functions on a new line + $s .= "\n"; + } + elseif ($type == KEYWORD_VAR && $type == $lastType) + { + // mutiple var-statements can go into one + $t = ',' . substr($t, 4); + } else + { + // add terminator $s .= ';'; + } } $s .= $t; $c++; + $lastType = $type; } } @@ -181,31 +279,41 @@ class JSMinPlus } break; + case KEYWORD_FUNCTION: + $s .= 'function' . ($n->name ? ' ' . $n->name : '') . '('; + $params = $n->params; + for ($i = 0, $j = count($params); $i < $j; $i++) + $s .= ($i ? ',' : '') . $params[$i]; + $s .= '){' . $this->parseTree($n->body, true) . '}'; + break; + case KEYWORD_IF: $s = 'if(' . $this->parseTree($n->condition) . ')'; $thenPart = $this->parseTree($n->thenPart); $elsePart = $n->elsePart ? $this->parseTree($n->elsePart) : null; - // quite a rancid hack to see if we should enclose the thenpart in brackets - if ($thenPart[0] != '{') - { - if (strpos($thenPart, 'if(') !== false) - $thenPart = '{' . $thenPart . '}'; - elseif ($elsePart) - $thenPart .= ';'; - } - - $s .= $thenPart; + // empty if-statement + if ($thenPart == '') + $thenPart = ';'; if ($elsePart) { - $s .= 'else'; + // be carefull and always make a block out of the thenPart; could be more optimized but is a lot of trouble + if ($thenPart != ';' && $thenPart[0] != '{') + $thenPart = '{' . $thenPart . '}'; + $s .= $thenPart . 'else'; + + // we could check for more, but that hardly ever applies so go for performance if ($elsePart[0] != '{') $s .= ' '; $s .= $elsePart; } + else + { + $s .= $thenPart; + } break; case KEYWORD_SWITCH: @@ -219,26 +327,48 @@ class JSMinPlus else $s .= 'default:'; - $statement = $this->parseTree($case->statements); + $statement = $this->parseTree($case->statements, true); if ($statement) - $s .= $statement . ';'; + { + $s .= $statement; + // no terminator for last statement + if ($i + 1 < $j) + $s .= ';'; + } } - $s = rtrim($s, ';') . '}'; + $s .= '}'; break; case KEYWORD_FOR: $s = 'for(' . ($n->setup ? $this->parseTree($n->setup) : '') . ';' . ($n->condition ? $this->parseTree($n->condition) : '') - . ';' . ($n->update ? $this->parseTree($n->update) : '') . ')' - . $this->parseTree($n->body); + . ';' . ($n->update ? $this->parseTree($n->update) : '') . ')'; + + $body = $this->parseTree($n->body); + if ($body == '') + $body = ';'; + + $s .= $body; break; case KEYWORD_WHILE: - $s = 'while(' . $this->parseTree($n->condition) . ')' . $this->parseTree($n->body); + $s = 'while(' . $this->parseTree($n->condition) . ')'; + + $body = $this->parseTree($n->body); + if ($body == '') + $body = ';'; + + $s .= $body; break; case JS_FOR_IN: - $s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body); + $s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')'; + + $body = $this->parseTree($n->body); + if ($body == '') + $body = ';'; + + $s .= $body; break; case KEYWORD_DO: @@ -263,11 +393,19 @@ class JSMinPlus break; case KEYWORD_THROW: - $s = 'throw ' . $this->parseTree($n->exception); - break; - case KEYWORD_RETURN: - $s = 'return' . ($n->value ? ' ' . $this->parseTree($n->value) : ''); + $s = $n->type; + if ($n->value) + { + $t = $this->parseTree($n->value); + if (strlen($t)) + { + if ($this->isWordChar($t[0]) || $t[0] == '\\') + $s .= ' '; + + $s .= $t; + } + } break; case KEYWORD_WITH: @@ -288,12 +426,47 @@ class JSMinPlus } break; + case KEYWORD_IN: + case KEYWORD_INSTANCEOF: + $left = $this->parseTree($n->treeNodes[0]); + $right = $this->parseTree($n->treeNodes[1]); + + $s = $left; + + if ($this->isWordChar(substr($left, -1))) + $s .= ' '; + + $s .= $n->type; + + if ($this->isWordChar($right[0]) || $right[0] == '\\') + $s .= ' '; + + $s .= $right; + break; + + case KEYWORD_DELETE: + case KEYWORD_TYPEOF: + $right = $this->parseTree($n->treeNodes[0]); + + $s = $n->type; + + if ($this->isWordChar($right[0]) || $right[0] == '\\') + $s .= ' '; + + $s .= $right; + break; + + case KEYWORD_VOID: + $s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')'; + break; + case KEYWORD_DEBUGGER: throw new Exception('NOT IMPLEMENTED: DEBUGGER'); break; - case TOKEN_CONDCOMMENT_MULTILINE: - $s = $n->value . ' '; + case TOKEN_CONDCOMMENT_START: + case TOKEN_CONDCOMMENT_END: + $s = $n->value . ($n->type == TOKEN_CONDCOMMENT_START ? ' ' : ''); $childs = $n->treeNodes; for ($i = 0, $j = count($childs); $i < $j; $i++) $s .= $this->parseTree($childs[$i]); @@ -333,34 +506,32 @@ class JSMinPlus case OP_PLUS: case OP_MINUS: - $s = $this->parseTree($n->treeNodes[0]) . $n->type; - $nextTokenType = $n->treeNodes[1]->type; - if ( $nextTokenType == OP_PLUS || $nextTokenType == OP_MINUS || - $nextTokenType == OP_INCREMENT || $nextTokenType == OP_DECREMENT || - $nextTokenType == OP_UNARY_PLUS || $nextTokenType == OP_UNARY_MINUS - ) - $s .= ' '; - $s .= $this->parseTree($n->treeNodes[1]); - break; + $left = $this->parseTree($n->treeNodes[0]); + $right = $this->parseTree($n->treeNodes[1]); - case KEYWORD_IN: - $s = $this->parseTree($n->treeNodes[0]) . ' in ' . $this->parseTree($n->treeNodes[1]); - break; + switch ($n->treeNodes[1]->type) + { + case OP_PLUS: + case OP_MINUS: + case OP_INCREMENT: + case OP_DECREMENT: + case OP_UNARY_PLUS: + case OP_UNARY_MINUS: + $s = $left . $n->type . ' ' . $right; + break; - case KEYWORD_INSTANCEOF: - $s = $this->parseTree($n->treeNodes[0]) . ' instanceof ' . $this->parseTree($n->treeNodes[1]); - break; + case TOKEN_STRING: + //combine concatted strings with same quotestyle + if ($n->type == OP_PLUS && substr($left, -1) == $right[0]) + { + $s = substr($left, 0, -1) . substr($right, 1); + break; + } + // FALL THROUGH - case KEYWORD_DELETE: - $s = 'delete ' . $this->parseTree($n->treeNodes[0]); - break; - - case KEYWORD_VOID: - $s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')'; - break; - - case KEYWORD_TYPEOF: - $s = 'typeof ' . $this->parseTree($n->treeNodes[0]); + default: + $s = $left . $n->type . $right; + } break; case OP_NOT: @@ -452,13 +623,33 @@ class JSMinPlus $s .= '}'; break; + case TOKEN_NUMBER: + $s = $n->value; + if (preg_match('/^([1-9]+)(0{3,})$/', $s, $m)) + $s = $m[1] . 'e' . strlen($m[2]); + break; + case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE: - case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP: + case TOKEN_IDENTIFIER: case TOKEN_STRING: case TOKEN_REGEXP: $s = $n->value; break; case JS_GROUP: - $s = '(' . $this->parseTree($n->treeNodes[0]) . ')'; + if (in_array( + $n->treeNodes[0]->type, + array( + JS_ARRAY_INIT, JS_OBJECT_INIT, JS_GROUP, + TOKEN_NUMBER, TOKEN_STRING, TOKEN_REGEXP, TOKEN_IDENTIFIER, + KEYWORD_NULL, KEYWORD_THIS, KEYWORD_TRUE, KEYWORD_FALSE + ) + )) + { + $s = $this->parseTree($n->treeNodes[0]); + } + else + { + $s = '(' . $this->parseTree($n->treeNodes[0]) . ')'; + } break; default: @@ -472,17 +663,23 @@ class JSMinPlus { return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved); } + + private function isWordChar($char) + { + return $char == '_' || $char == '$' || ctype_alnum($char); + } } class JSParser { private $t; + private $minifier; private $opPrecedence = array( ';' => 0, ',' => 1, '=' => 2, '?' => 2, ':' => 2, - // The above all have to have the same precedence, see bug 330975. + // The above all have to have the same precedence, see bug 330975 '||' => 4, '&&' => 5, '|' => 6, @@ -523,11 +720,12 @@ class JSParser '.' => 2, JS_NEW_WITH_ARGS => 2, JS_INDEX => 2, JS_CALL => 2, JS_ARRAY_INIT => 1, JS_OBJECT_INIT => 1, JS_GROUP => 1, - TOKEN_CONDCOMMENT_MULTILINE => 1 + TOKEN_CONDCOMMENT_START => 1, TOKEN_CONDCOMMENT_END => 1 ); - public function __construct() + public function __construct($minifier=null) { + $this->minifier = $minifier; $this->t = new JSTokenizer(); } @@ -551,6 +749,19 @@ class JSParser $n->funDecls = $x->funDecls; $n->varDecls = $x->varDecls; + // minify by scope + if ($this->minifier) + { + $n->value = $this->minifier->parseTree($n); + + // clear tree from node to save memory + $n->treeNodes = null; + $n->funDecls = null; + $n->varDecls = null; + + $n->type = JS_MINIFIED; + } + return $n; } @@ -809,7 +1020,7 @@ class JSParser case KEYWORD_THROW: $n = new JSNode($this->t); - $n->exception = $this->Expression($x); + $n->value = $this->Expression($x); break; case KEYWORD_RETURN: @@ -835,7 +1046,8 @@ class JSParser $n = $this->Variables($x); break; - case TOKEN_CONDCOMMENT_MULTILINE: + case TOKEN_CONDCOMMENT_START: + case TOKEN_CONDCOMMENT_END: $n = new JSNode($this->t); return $n; @@ -994,37 +1206,47 @@ class JSParser // NB: cannot be empty, Statement handled that. break 2; - case OP_ASSIGN: case OP_HOOK: - case OP_COLON: if ($this->t->scanOperand) break 2; - // Use >, not >=, for right-associative ASSIGN and HOOK/COLON. while ( !empty($operators) && - ( $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt] || - ($tt == OP_COLON && end($operators)->type == OP_ASSIGN) - ) + $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt] ) $this->reduce($operators, $operands); - if ($tt == OP_COLON) - { - $n = end($operators); - if ($n->type != OP_HOOK) - throw $this->t->newSyntaxError('Invalid label'); + array_push($operators, new JSNode($this->t)); - --$x->hookLevel; - } - else - { - array_push($operators, new JSNode($this->t)); - if ($tt == OP_ASSIGN) - end($operands)->assignOp = $this->t->currentToken()->assignOp; - else - ++$x->hookLevel; - } + ++$x->hookLevel; + $this->t->scanOperand = true; + $n = $this->Expression($x); + if (!$this->t->match(OP_COLON)) + break 2; + + --$x->hookLevel; + array_push($operands, $n); + break; + + case OP_COLON: + if ($x->hookLevel) + break 2; + + throw $this->t->newSyntaxError('Invalid label'); + break; + + case OP_ASSIGN: + if ($this->t->scanOperand) + break 2; + + // Use >, not >=, for right-associative ASSIGN + while ( !empty($operators) && + $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt] + ) + $this->reduce($operators, $operands); + + array_push($operators, new JSNode($this->t)); + end($operands)->assignOp = $this->t->currentToken()->assignOp; $this->t->scanOperand = true; break; @@ -1036,14 +1258,19 @@ class JSParser !$x->bracketLevel && !$x->curlyLevel && !$x->parenLevel ) - { break 2; - } // FALL THROUGH case OP_COMMA: - // Treat comma as left-associative so reduce can fold left-heavy - // COMMA trees into a single array. - // FALL THROUGH + // A comma operator should not be parsed if we're parsing the then part + // of a conditional expression unless it's parenthesized somehow. + if ($tt == OP_COMMA && $x->hookLevel && + !$x->bracketLevel && !$x->curlyLevel && + !$x->parenLevel + ) + break 2; + // Treat comma as left-associative so reduce can fold left-heavy + // COMMA trees into a single array. + // FALL THROUGH case OP_OR: case OP_AND: case OP_BITWISE_OR: @@ -1127,7 +1354,8 @@ class JSParser $this->t->scanOperand = false; break; - case TOKEN_CONDCOMMENT_MULTILINE: + case TOKEN_CONDCOMMENT_START: + case TOKEN_CONDCOMMENT_END: if ($this->t->scanOperand) array_push($operators, new JSNode($this->t)); else @@ -1312,7 +1540,7 @@ class JSParser } if ($x->hookLevel != $hl) - throw $this->t->newSyntaxError('Missing : after ?'); + throw $this->t->newSyntaxError('Missing : in conditional expression'); if ($x->parenLevel != $pl) throw $this->t->newSyntaxError('Missing ) in parenthetical'); @@ -1443,7 +1671,7 @@ class JSNode if (($numargs = func_num_args()) > 2) { - $args = func_get_args();; + $args = func_get_args(); for ($i = 2; $i < $numargs; $i++) $this->addNode($args[$i]); } @@ -1465,6 +1693,14 @@ class JSNode public function addNode($node) { + if ($node !== null) + { + if ($node->start < $this->start) + $this->start = $node->start; + if ($this->end < $node->end) + $this->end = $node->end; + } + $this->treeNodes[] = $node; } } @@ -1499,44 +1735,11 @@ class JSTokenizer ); private $opTypeNames = array( - ';' => 'SEMICOLON', - ',' => 'COMMA', - '?' => 'HOOK', - ':' => 'COLON', - '||' => 'OR', - '&&' => 'AND', - '|' => 'BITWISE_OR', - '^' => 'BITWISE_XOR', - '&' => 'BITWISE_AND', - '===' => 'STRICT_EQ', - '==' => 'EQ', - '=' => 'ASSIGN', - '!==' => 'STRICT_NE', - '!=' => 'NE', - '<<' => 'LSH', - '<=' => 'LE', - '<' => 'LT', - '>>>' => 'URSH', - '>>' => 'RSH', - '>=' => 'GE', - '>' => 'GT', - '++' => 'INCREMENT', - '--' => 'DECREMENT', - '+' => 'PLUS', - '-' => 'MINUS', - '*' => 'MUL', - '/' => 'DIV', - '%' => 'MOD', - '!' => 'NOT', - '~' => 'BITWISE_NOT', - '.' => 'DOT', - '[' => 'LEFT_BRACKET', - ']' => 'RIGHT_BRACKET', - '{' => 'LEFT_CURLY', - '}' => 'RIGHT_CURLY', - '(' => 'LEFT_PAREN', - ')' => 'RIGHT_PAREN', - '@*/' => 'CONDCOMMENT_END' + ';', ',', '?', ':', '||', '&&', '|', '^', + '&', '===', '==', '=', '!==', '!=', '<<', '<=', + '<', '>>>', '>>', '>=', '>', '++', '--', '+', + '-', '*', '/', '%', '!', '~', '.', '[', + ']', '{', '}', '(', ')', '@*/' ); private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%'); @@ -1544,17 +1747,7 @@ class JSTokenizer public function __construct() { - $this->opRegExp = '#^(' . implode('|', array_map('preg_quote', array_keys($this->opTypeNames))) . ')#'; - - // this is quite a hidden yet convenient place to create the defines for operators and keywords - foreach ($this->opTypeNames as $operand => $name) - define('OP_' . $name, $operand); - - define('OP_UNARY_PLUS', 'U+'); - define('OP_UNARY_MINUS', 'U-'); - - foreach ($this->keywords as $keyword) - define('KEYWORD_' . strtoupper($keyword), $keyword); + $this->opRegExp = '#^(' . implode('|', array_map('preg_quote', $this->opTypeNames)) . ')#'; } public function init($source, $filename = '', $lineno = 1) @@ -1668,7 +1861,7 @@ class JSTokenizer } // Comments - if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?(?:.|\n)*?\*\/|\/.*)/', $input, $match)) + if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?.*?\*\/|\/[^\n]*)/s', $input, $match)) { if (!$chunksize) break; @@ -1681,7 +1874,7 @@ class JSTokenizer // check if this is a conditional (JScript) comment if (!empty($match[1])) { - //$match[0] = '/*' . $match[1]; + $match[0] = '/*' . $match[1]; $conditional_comment = true; break; } @@ -1699,28 +1892,44 @@ class JSTokenizer } elseif ($conditional_comment) { - $tt = TOKEN_CONDCOMMENT_MULTILINE; + $tt = TOKEN_CONDCOMMENT_START; } else { switch ($input[0]) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - if (preg_match('/^\d+\.\d*(?:[eE][-+]?\d+)?|^\d+(?:\.\d*)?[eE][-+]?\d+/', $input, $match)) + case '0': + // hexadecimal + if (($input[1] == 'x' || $input[1] == 'X') && preg_match('/^0x[0-9a-f]+/i', $input, $match)) { $tt = TOKEN_NUMBER; + break; } - elseif (preg_match('/^0[xX][\da-fA-F]+|^0[0-7]*|^\d+/', $input, $match)) + // FALL THROUGH + + case '1': case '2': case '3': case '4': case '5': + case '6': case '7': case '8': case '9': + // should always match + preg_match('/^\d+(?:\.\d*)?(?:[eE][-+]?\d+)?/', $input, $match); + $tt = TOKEN_NUMBER; + break; + + case "'": + if (preg_match('/^\'(?:[^\\\\\'\r\n]++|\\\\(?:.|\r?\n))*\'/', $input, $match)) { - // this should always match because of \d+ - $tt = TOKEN_NUMBER; + $tt = TOKEN_STRING; + } + else + { + if ($chunksize) + return $this->get(null); // retry with a full chunk fetch + + throw $this->newSyntaxError('Unterminated string literal'); } break; case '"': - case "'": - if (preg_match('/^"(?:\\\\(?:.|\r?\n)|[^\\\\"\r\n])*"|^\'(?:\\\\(?:.|\r?\n)|[^\\\\\'\r\n])*\'/', $input, $match)) + if (preg_match('/^"(?:[^\\\\"\r\n]++|\\\\(?:.|\r?\n))*"/', $input, $match)) { $tt = TOKEN_STRING; } @@ -1739,7 +1948,7 @@ class JSTokenizer $tt = TOKEN_REGEXP; break; } - // fall through + // FALL THROUGH case '|': case '^': @@ -1780,7 +1989,7 @@ class JSTokenizer $tt = TOKEN_NUMBER; break; } - // fall through + // FALL THROUGH case ';': case ',': @@ -1799,7 +2008,14 @@ class JSTokenizer break; case '@': - throw $this->newSyntaxError('Illegal token'); + // check end of conditional comment + if (substr($input, 0, 3) == '@*/') + { + $match = array('@*/'); + $tt = TOKEN_CONDCOMMENT_END; + } + else + throw $this->newSyntaxError('Illegal token'); break; case "\n": @@ -1868,5 +2084,3 @@ class JSToken public $lineno; public $assignOp; } - -?> diff --git a/lib/minify/lib/Minify.php b/lib/minify/lib/Minify.php index 25f0827a059..9634f221fbd 100644 --- a/lib/minify/lib/Minify.php +++ b/lib/minify/lib/Minify.php @@ -29,12 +29,13 @@ require_once 'Minify/Source.php'; */ class Minify { - const VERSION = '2.1.3'; + const VERSION = '2.1.5'; 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? @@ -58,7 +59,14 @@ class Minify { * @var string $importWarning */ public static $importWarning = "/* See http://code.google.com/p/minify/wiki/CommonProblems#@imports_can_appear_in_invalid_locations_in_combined_CSS_files */\n"; - + + /** + * Has the DOCUMENT_ROOT been set in user code? + * + * @var bool + */ + public static $isDocRootSet = false; + /** * Specify a cache object (with identical interface as Minify_Cache_File) or * a path to use with Minify_Cache_File. @@ -148,8 +156,8 @@ class Minify { * * Any controller options are documented in that controller's setupSources() method. * - * @param mixed instance of subclass of Minify_Controller_Base or string name of - * controller. E.g. 'Files' + * @param mixed $controller instance of subclass of Minify_Controller_Base or string + * name of controller. E.g. 'Files' * * @param array $options controller/serve options * @@ -159,6 +167,10 @@ class Minify { */ public static function serve($controller, $options = array()) { + if (! self::$isDocRootSet && 0 === stripos(PHP_OS, 'win')) { + self::setDocRoot(); + } + if (is_string($controller)) { // make $controller into object $class = 'Minify_Controller_' . $controller; @@ -167,6 +179,7 @@ class Minify { . str_replace('_', '/', $controller) . ".php"; } $controller = new $class(); + /* @var Minify_Controller_Base $controller */ } // set up controller sources and mix remaining options with @@ -179,9 +192,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 +213,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 +224,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 +239,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) { @@ -249,8 +264,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 +275,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 +290,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 +307,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 +325,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'; } @@ -356,52 +389,69 @@ class Minify { } /** - * On IIS, create $_SERVER['DOCUMENT_ROOT'] + * Set $_SERVER['DOCUMENT_ROOT']. On IIS, the value is created from SCRIPT_FILENAME and SCRIPT_NAME. * - * @param bool $unsetPathInfo (default false) if true, $_SERVER['PATH_INFO'] - * will be unset (it is inconsistent with Apache's setting) - * - * @return null + * @param string $docRoot value to use for DOCUMENT_ROOT */ - public static function setDocRoot($unsetPathInfo = false) + public static function setDocRoot($docRoot = '') { - if (isset($_SERVER['SERVER_SOFTWARE']) - && 0 === strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/') - ) { - $_SERVER['DOCUMENT_ROOT'] = rtrim(substr( - $_SERVER['PATH_TRANSLATED'] + self::$isDocRootSet = true; + if ($docRoot) { + $_SERVER['DOCUMENT_ROOT'] = $docRoot; + } elseif (isset($_SERVER['SERVER_SOFTWARE']) + && 0 === strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/')) { + $_SERVER['DOCUMENT_ROOT'] = substr( + $_SERVER['SCRIPT_FILENAME'] ,0 - ,strlen($_SERVER['PATH_TRANSLATED']) - strlen($_SERVER['SCRIPT_NAME']) - ), '\\'); - if ($unsetPathInfo) { - unset($_SERVER['PATH_INFO']); - } - require_once 'Minify/Logger.php'; - Minify_Logger::log("setDocRoot() set DOCUMENT_ROOT to \"{$_SERVER['DOCUMENT_ROOT']}\""); + ,strlen($_SERVER['SCRIPT_FILENAME']) - strlen($_SERVER['SCRIPT_NAME'])); + $_SERVER['DOCUMENT_ROOT'] = rtrim($_SERVER['DOCUMENT_ROOT'], '\\'); } } /** - * @var mixed Minify_Cache_* object or null (i.e. no server cache is used) + * Any Minify_Cache_* object or null (i.e. no server cache is used) + * + * @var Minify_Cache_File */ private static $_cache = null; /** - * @var Minify_Controller active controller for current request + * Active controller for current request + * + * @var Minify_Controller_Base */ protected static $_controller = null; /** - * @var array options for current request + * Options for current request + * + * @var array */ protected static $_options = null; - + + /** + * @param string $header + * + * @param string $url + */ + 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 "

    $h1

    "; + echo "

    Please see $url.

    "; + exit(); + } + /** * Set up sources to use Minify_Lines * * @param array $sources Minify_Source instances - * - * @return null */ protected static function _setupDebug($sources) { @@ -439,21 +489,22 @@ class Minify { $defaultMinifier = isset(self::$_options['minifiers'][$type]) ? 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) { - $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) { + + // process groups of sources with identical minifiers/options + $content = array(); + $i = 0; + $l = count(self::$_controller->sources); + $groupToProcessTogether = array(); + $lastMinifier = null; + $lastOptions = null; + do { + // get next source + $source = null; + if ($i < $l) { + $source = self::$_controller->sources[$i]; + /* @var Minify_Source $source */ + $sourceContent = $source->getContent(); + // allow the source to override our minifier and options $minifier = (null !== $source->minifier) ? $source->minifier @@ -461,16 +512,40 @@ class Minify { $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); + } + // do we need to process our group right now? + if ($i > 0 // yes, we have at least the first group populated + && ( + ! $source // yes, we ran out of sources + || $type === self::TYPE_CSS // yes, to process CSS individually (avoiding PCRE bugs/limits) + || $minifier !== $lastMinifier // yes, minifier changed + || $options !== $lastOptions) // yes, options changed + ) + { + // minify previous sources with last settings + $imploded = implode($implodeSeparator, $groupToProcessTogether); + $groupToProcessTogether = array(); + if ($lastMinifier) { + self::$_controller->loadMinifier($lastMinifier); + try { + $content[] = call_user_func($lastMinifier, $imploded, $lastOptions); + } catch (Exception $e) { + throw new Exception("Exception in minifier: " . $e->getMessage()); + } } else { - $pieces[] = $source->getContent(); + $content[] = $imploded; } } - $content = implode($implodeSeparator, $pieces); - } + // add content to the group + if ($source) { + $groupToProcessTogether[] = $sourceContent; + $lastMinifier = $minifier; + $lastOptions = $options; + } + $i++; + } while ($source); + + $content = implode($implodeSeparator, $content); if ($type === self::TYPE_CSS && false !== strpos($content, '@import')) { $content = self::_handleCssImports($content); @@ -491,22 +566,32 @@ 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'] + ,self::VERSION ))); + return "{$prefix}_{$name}_{$md5}"; } /** - * Bubble CSS @imports to the top or prepend a warning if an - * @import is detected not at the top. + * Bubble CSS @imports to the top or prepend a warning if an import is detected not at the top. + * + * @param string $css + * + * @return string */ protected static function _handleCssImports($css) { diff --git a/lib/minify/lib/Minify/CSS.php b/lib/minify/lib/Minify/CSS.php index b2d8e0bf0b7..49882e9d2d5 100644 --- a/lib/minify/lib/Minify/CSS.php +++ b/lib/minify/lib/Minify/CSS.php @@ -1,83 +1,99 @@ - - * @author http://code.google.com/u/1stvamp/ (Issue 64 patch) - */ -class Minify_CSS { - - /** - * Minify a CSS string - * - * @param string $css - * - * @param array $options available options: - * - * 'preserveComments': (default true) multi-line comments that begin - * with "/*!" will be preserved with newlines before and after to - * enhance readability. - * - * 'prependRelativePath': (default null) if given, this string will be - * prepended to all relative URIs in import/url declarations - * - * 'currentDir': (default null) if given, this is assumed to be the - * directory of the current CSS file. Using this, minify will rewrite - * all relative URIs in import/url declarations to correctly point to - * 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 - * - * - * @return string - */ - public static function minify($css, $options = array()) - { - require_once 'Minify/CSS/Compressor.php'; - if (isset($options['preserveComments']) - && !$options['preserveComments']) { - $css = Minify_CSS_Compressor::process($css, $options); - } else { - require_once 'Minify/CommentPreserver.php'; - $css = Minify_CommentPreserver::process( - $css - ,array('Minify_CSS_Compressor', 'process') - ,array($options) - ); - } - if (! isset($options['currentDir']) && ! isset($options['prependRelativePath'])) { - return $css; - } - require_once 'Minify/CSS/UriRewriter.php'; - if (isset($options['currentDir'])) { - return Minify_CSS_UriRewriter::rewrite( - $css - ,$options['currentDir'] - ,isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT'] - ,isset($options['symlinks']) ? $options['symlinks'] : array() - ); - } else { - return Minify_CSS_UriRewriter::prepend( - $css - ,$options['prependRelativePath'] - ); - } - } -} + + * @author http://code.google.com/u/1stvamp/ (Issue 64 patch) + */ +class Minify_CSS { + + /** + * Minify a CSS string + * + * @param string $css + * + * @param array $options available options: + * + * '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 + * + * 'currentDir': (default null) if given, this is assumed to be the + * directory of the current CSS file. Using this, minify will rewrite + * all relative URIs in import/url declarations to correctly point to + * 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 + * + * + * '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 (! $options['preserveComments']) { + $css = Minify_CSS_Compressor::process($css, $options); + } else { + require_once 'Minify/CommentPreserver.php'; + $css = Minify_CommentPreserver::process( + $css + ,array('Minify_CSS_Compressor', 'process') + ,array($options) + ); + } + if (! $options['currentDir'] && ! $options['prependRelativePath']) { + return $css; + } + require_once 'Minify/CSS/UriRewriter.php'; + if ($options['currentDir']) { + return Minify_CSS_UriRewriter::rewrite( + $css + ,$options['currentDir'] + ,$options['docRoot'] + ,$options['symlinks'] + ); + } else { + return Minify_CSS_UriRewriter::prepend( + $css + ,$options['prependRelativePath'] + ); + } + } +} diff --git a/lib/minify/lib/Minify/CSS/Compressor.php b/lib/minify/lib/Minify/CSS/Compressor.php index dad700a9c4e..c6cdd8b7a6d 100644 --- a/lib/minify/lib/Minify/CSS/Compressor.php +++ b/lib/minify/lib/Minify/CSS/Compressor.php @@ -1,251 +1,249 @@ - - * @author http://code.google.com/u/1stvamp/ (Issue 64 patch) - */ -class Minify_CSS_Compressor { - - /** - * Minify a CSS string - * - * @param string $css - * - * @param array $options (currently ignored) - * - * @return string - */ - public static function process($css, $options = array()) - { - $obj = new Minify_CSS_Compressor($options); - return $obj->_process($css); - } - - /** - * @var array options - */ - protected $_options = null; - - /** - * @var bool Are we "in" a hack? - * - * I.e. are some browsers targetted until the next comment? - */ - protected $_inHack = false; - - - /** - * Constructor - * - * @param array $options (currently ignored) - * - * @return null - */ - private function __construct($options) { - $this->_options = $options; - } - - /** - * Minify a CSS string - * - * @param string $css - * - * @return string - */ - protected function _process($css) - { - $css = str_replace("\r\n", "\n", $css); - - // preserve empty comment after '>' - // http://www.webdevout.net/css-hacks#in_css-selectors - $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css); - - // preserve empty comment between property and value - // http://css-discuss.incutio.com/?page=BoxModelHack - $css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css); - $css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css); - - // apply callback to all valid comments (and strip out surrounding ws - $css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@' - ,array($this, '_commentCB'), $css); - - // remove ws around { } and last semicolon in declaration block - $css = preg_replace('/\\s*{\\s*/', '{', $css); - $css = preg_replace('/;?\\s*}\\s*/', '}', $css); - - // remove ws surrounding semicolons - $css = preg_replace('/\\s*;\\s*/', ';', $css); - - // remove ws around urls - $css = preg_replace('/ - url\\( # url( - \\s* - ([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis) - \\s* - \\) # ) - /x', 'url($1)', $css); - - // remove ws between rules and colons - $css = preg_replace('/ - \\s* - ([{;]) # 1 = beginning of block or rule separator - \\s* - ([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter) - \\s* - : - \\s* - (\\b|[#\'"]) # 3 = first character of a value - /x', '$1$2:$3', $css); - - // remove ws in selectors - $css = preg_replace_callback('/ - (?: # non-capture - \\s* - [^~>+,\\s]+ # selector part - \\s* - [,>+~] # combinators - )+ - \\s* - [^~>+,\\s]+ # selector part - { # open declaration block - /x' - ,array($this, '_selectorsCB'), $css); - - // minimize hex colors - $css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i' - , '$1#$2$3$4$5', $css); - - // remove spaces between font families - $css = preg_replace_callback('/font-family:([^;}]+)([;}])/' - ,array($this, '_fontFamilyCB'), $css); - - $css = preg_replace('/@import\\s+url/', '@import url', $css); - - // replace any ws involving newlines with a single newline - $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css); - - // separate common descendent selectors w/ newlines (to limit line lengths) - $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css); - - // Use newline after 1st numeric value (to limit line lengths). - $css = preg_replace('/ - ((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value - \\s+ - /x' - ,"$1\n", $css); - - // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/ - $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css); - - return trim($css); - } - - /** - * Replace what looks like a set of selectors - * - * @param array $m regex matches - * - * @return string - */ - protected function _selectorsCB($m) - { - // remove ws around the combinators - return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]); - } - - /** - * Process a comment and return a replacement - * - * @param array $m regex matches - * - * @return string - */ - protected function _commentCB($m) - { - $hasSurroundingWs = (trim($m[0]) !== $m[1]); - $m = $m[1]; - // $m is the comment content w/o the surrounding tokens, - // but the return value will replace the entire comment. - if ($m === 'keep') { - return '/**/'; - } - if ($m === '" "') { - // component of http://tantek.com/CSS/Examples/midpass.html - return '/*" "*/'; - } - if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) { - // component of http://tantek.com/CSS/Examples/midpass.html - return '/*";}}/* */'; - } - if ($this->_inHack) { - // inversion: feeding only to one browser - if (preg_match('@ - ^/ # comment started like /*/ - \\s* - (\\S[\\s\\S]+?) # has at least some non-ws content - \\s* - /\\* # ends like /*/ or /**/ - @x', $m, $n)) { - // end hack mode after this comment, but preserve the hack and comment content - $this->_inHack = false; - return "/*/{$n[1]}/**/"; - } - } - if (substr($m, -1) === '\\') { // comment ends like \*/ - // begin hack mode and preserve hack - $this->_inHack = true; - return '/*\\*/'; - } - if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */ - // begin hack mode and preserve hack - $this->_inHack = true; - return '/*/*/'; - } - if ($this->_inHack) { - // a regular comment ends hack mode but should be preserved - $this->_inHack = false; - return '/**/'; - } - // Issue 107: if there's any surrounding whitespace, it may be important, so - // replace the comment with a single space - return $hasSurroundingWs // remove all other comments - ? ' ' - : ''; - } - - /** - * Process a font-family listing and return a replacement - * - * @param array $m regex matches - * - * @return string - */ - protected function _fontFamilyCB($m) - { - // 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]; - } -} + + * @author http://code.google.com/u/1stvamp/ (Issue 64 patch) + */ +class Minify_CSS_Compressor { + + /** + * Minify a CSS string + * + * @param string $css + * + * @param array $options (currently ignored) + * + * @return string + */ + public static function process($css, $options = array()) + { + $obj = new Minify_CSS_Compressor($options); + return $obj->_process($css); + } + + /** + * @var array + */ + protected $_options = null; + + /** + * Are we "in" a hack? I.e. are some browsers targetted until the next comment? + * + * @var bool + */ + protected $_inHack = false; + + + /** + * Constructor + * + * @param array $options (currently ignored) + */ + private function __construct($options) { + $this->_options = $options; + } + + /** + * Minify a CSS string + * + * @param string $css + * + * @return string + */ + protected function _process($css) + { + $css = str_replace("\r\n", "\n", $css); + + // preserve empty comment after '>' + // http://www.webdevout.net/css-hacks#in_css-selectors + $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css); + + // preserve empty comment between property and value + // http://css-discuss.incutio.com/?page=BoxModelHack + $css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css); + $css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css); + + // apply callback to all valid comments (and strip out surrounding ws + $css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@' + ,array($this, '_commentCB'), $css); + + // remove ws around { } and last semicolon in declaration block + $css = preg_replace('/\\s*{\\s*/', '{', $css); + $css = preg_replace('/;?\\s*}\\s*/', '}', $css); + + // remove ws surrounding semicolons + $css = preg_replace('/\\s*;\\s*/', ';', $css); + + // remove ws around urls + $css = preg_replace('/ + url\\( # url( + \\s* + ([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis) + \\s* + \\) # ) + /x', 'url($1)', $css); + + // remove ws between rules and colons + $css = preg_replace('/ + \\s* + ([{;]) # 1 = beginning of block or rule separator + \\s* + ([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter) + \\s* + : + \\s* + (\\b|[#\'"-]) # 3 = first character of a value + /x', '$1$2:$3', $css); + + // remove ws in selectors + $css = preg_replace_callback('/ + (?: # non-capture + \\s* + [^~>+,\\s]+ # selector part + \\s* + [,>+~] # combinators + )+ + \\s* + [^~>+,\\s]+ # selector part + { # open declaration block + /x' + ,array($this, '_selectorsCB'), $css); + + // minimize hex colors + $css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i' + , '$1#$2$3$4$5', $css); + + // remove spaces between font families + $css = preg_replace_callback('/font-family:([^;}]+)([;}])/' + ,array($this, '_fontFamilyCB'), $css); + + $css = preg_replace('/@import\\s+url/', '@import url', $css); + + // replace any ws involving newlines with a single newline + $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css); + + // separate common descendent selectors w/ newlines (to limit line lengths) + $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css); + + // Use newline after 1st numeric value (to limit line lengths). + $css = preg_replace('/ + ((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value + \\s+ + /x' + ,"$1\n", $css); + + // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/ + $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css); + + return trim($css); + } + + /** + * Replace what looks like a set of selectors + * + * @param array $m regex matches + * + * @return string + */ + protected function _selectorsCB($m) + { + // remove ws around the combinators + return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]); + } + + /** + * Process a comment and return a replacement + * + * @param array $m regex matches + * + * @return string + */ + protected function _commentCB($m) + { + $hasSurroundingWs = (trim($m[0]) !== $m[1]); + $m = $m[1]; + // $m is the comment content w/o the surrounding tokens, + // but the return value will replace the entire comment. + if ($m === 'keep') { + return '/**/'; + } + if ($m === '" "') { + // component of http://tantek.com/CSS/Examples/midpass.html + return '/*" "*/'; + } + if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) { + // component of http://tantek.com/CSS/Examples/midpass.html + return '/*";}}/* */'; + } + if ($this->_inHack) { + // inversion: feeding only to one browser + if (preg_match('@ + ^/ # comment started like /*/ + \\s* + (\\S[\\s\\S]+?) # has at least some non-ws content + \\s* + /\\* # ends like /*/ or /**/ + @x', $m, $n)) { + // end hack mode after this comment, but preserve the hack and comment content + $this->_inHack = false; + return "/*/{$n[1]}/**/"; + } + } + if (substr($m, -1) === '\\') { // comment ends like \*/ + // begin hack mode and preserve hack + $this->_inHack = true; + return '/*\\*/'; + } + if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */ + // begin hack mode and preserve hack + $this->_inHack = true; + return '/*/*/'; + } + if ($this->_inHack) { + // a regular comment ends hack mode but should be preserved + $this->_inHack = false; + return '/**/'; + } + // Issue 107: if there's any surrounding whitespace, it may be important, so + // replace the comment with a single space + return $hasSurroundingWs // remove all other comments + ? ' ' + : ''; + } + + /** + * Process a font-family listing and return a replacement + * + * @param array $m regex matches + * + * @return string + */ + protected function _fontFamilyCB($m) + { + // 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/lib/minify/lib/Minify/CSS/UriRewriter.php b/lib/minify/lib/Minify/CSS/UriRewriter.php index 2b47cfc1d07..8845f1561f5 100644 --- a/lib/minify/lib/Minify/CSS/UriRewriter.php +++ b/lib/minify/lib/Minify/CSS/UriRewriter.php @@ -12,21 +12,15 @@ */ 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 */ 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 +77,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 +101,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( @@ -236,21 +165,39 @@ class Minify_CSS_UriRewriter { self::$debugText .= "docroot stripped : {$path}\n"; // fix to root-relative URI - $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 +214,97 @@ class Minify_CSS_UriRewriter { } return rtrim($path, '/\\'); } + + /** + * Directory of this stylesheet + * + * @var string + */ + private static $_currentDir = ''; + + /** + * DOC_ROOT + * + * @var string + */ + private static $_docRoot = ''; + + /** + * directory replacements to map symlink targets back to their + * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath' + * + * @var array + */ + private static $_symlinks = array(); + + /** + * Path to prepend + * + * @var string + */ + private static $_prependPath = null; + + /** + * @param string $css + * + * @return string + */ + private static function _trimUrls($css) + { + return preg_replace('/ + url\\( # url( + \\s* + ([^\\)]+?) # 1 = URI (assuming does not contain ")") + \\s* + \\) # ) + /x', 'url($1)', $css); + } + + /** + * @param array $m + * + * @return string + */ + 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/lib/minify/lib/Minify/Cache/APC.php b/lib/minify/lib/Minify/Cache/APC.php index ca84d299871..24ab0462052 100644 --- a/lib/minify/lib/Minify/Cache/APC.php +++ b/lib/minify/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/lib/minify/lib/Minify/Cache/File.php b/lib/minify/lib/Minify/Cache/File.php index 2753e6b8e90..3fa6b18fcb2 100644 --- a/lib/minify/lib/Minify/Cache/File.php +++ b/lib/minify/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. * @@ -30,15 +29,14 @@ class Minify_Cache_File { $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; + $file = $this->_path . '/' . $id; + if (! @file_put_contents($file, $data, $flag)) { + $this->_log("Minify_Cache_File: Write failed to '$file'"); } // write control if ($data !== $this->fetch($id)) { @unlink($file); + $this->_log("Minify_Cache_File: Post-write read failed for '$file'"); return false; } return true; @@ -119,6 +117,78 @@ 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; diff --git a/lib/minify/lib/Minify/Cache/Memcache.php b/lib/minify/lib/Minify/Cache/Memcache.php index 2b81e7a3294..72bf454b93a 100644 --- a/lib/minify/lib/Minify/Cache/Memcache.php +++ b/lib/minify/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/lib/minify/lib/Minify/Cache/ZendPlatform.php b/lib/minify/lib/Minify/Cache/ZendPlatform.php new file mode 100644 index 00000000000..3130d69a7b5 --- /dev/null +++ b/lib/minify/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/lib/minify/lib/Minify/CommentPreserver.php b/lib/minify/lib/Minify/CommentPreserver.php index f56eb3461c3..7a359bf9bb5 100644 --- a/lib/minify/lib/Minify/CommentPreserver.php +++ b/lib/minify/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/lib/minify/lib/Minify/Controller/Base.php b/lib/minify/lib/Minify/Controller/Base.php index 84889b3f0c5..240b54495b8 100644 --- a/lib/minify/lib/Minify/Controller/Base.php +++ b/lib/minify/lib/Minify/Controller/Base.php @@ -27,7 +27,7 @@ abstract class Minify_Controller_Base { * * @param array $options controller and Minify options * - * return array $options Minify::serve options + * @return array $options Minify::serve options */ abstract public function setupSources($options); @@ -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,15 +137,58 @@ abstract class Minify_Controller_Base { list($revExt) = explode('.', strrev($base)); return in_array(strrev($revExt), array('js', 'css', 'html', 'txt')); } - + /** - * @var array instances of Minify_Source, which provide content and - * any individual minification needs. + * @param string $file + * @param array $allowDirs + * @param string $uri + * @return bool + * @throws Exception + */ + 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) . "';"); + } + + /** + * @param string $file + * @throws Exception + */ + public static function checkNotHidden($file) + { + $b = basename($file); + if (0 === strpos($b, '.')) { + throw new Exception("Filename '$b' starts with period (may be hidden)"); + } + } + + /** + * instances of Minify_Source, which provide content and any individual minification needs. + * + * @var array * * @see Minify_Source */ public $sources = array(); + /** + * Short name to place inside cache id + * + * 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 + */ + public $selectionId = ''; + /** * Mix in default controller options with user-given options * @@ -192,10 +238,12 @@ abstract class Minify_Controller_Base { /** * Send message to the Minify logger + * * @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/lib/minify/lib/Minify/Controller/Groups.php b/lib/minify/lib/Minify/Controller/Groups.php index 1ac57703ad8..2d4e43b2f7b 100644 --- a/lib/minify/lib/Minify/Controller/Groups.php +++ b/lib/minify/lib/Minify/Controller/Groups.php @@ -34,12 +34,11 @@ class Minify_Controller_Groups extends Minify_Controller_Base { * Set up groups of files as sources * * @param array $options controller and Minify options - * @return array Minify options - * - * Controller options: - * + * * 'groups': (required) array mapping PATH_INFO strings to arrays - * of complete file paths. @see Minify_Controller_Groups + * of complete file paths. @see Minify_Controller_Groups + * + * @return array Minify options */ public function setupSources($options) { // strip controller options diff --git a/lib/minify/lib/Minify/Controller/MinApp.php b/lib/minify/lib/Minify/Controller/MinApp.php index 9582d292caa..d47c60d9f78 100644 --- a/lib/minify/lib/Minify/Controller/MinApp.php +++ b/lib/minify/lib/Minify/Controller/MinApp.php @@ -18,8 +18,8 @@ class Minify_Controller_MinApp extends Minify_Controller_Base { * Set up groups of files as sources * * @param array $options controller and Minify options + * * @return array Minify options - * */ public function setupSources($options) { // filter controller options @@ -28,64 +28,94 @@ 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 = ''; + $firstMissingResource = null; + 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)"); + $keys = explode(',', $_GET['g']); + foreach ($keys 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)"); + if (null === $firstMissingResource) { + $firstMissingResource = basename($file); + continue; + } else { + $secondMissingResource = basename($file); + $this->log("More than one file was missing: '$firstMissingResource', '$secondMissingResource'"); + 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 +126,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 +136,96 @@ 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 (null === $firstMissingResource) { + $firstMissingResource = $uri; + continue; + } else { + $secondMissingResource = $uri; + $this->log("More than one file was missing: '$firstMissingResource', '$secondMissingResource`'"); + 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 (null !== $firstMissingResource) { + array_unshift($sources, new Minify_Source(array( + 'id' => 'missingFile' + // should not cause cache invalidation + ,'lastModified' => 0 + // due to caching, filename is unreliable. + ,'content' => "/* Minify: at least one missing file. See " . Minify::URL_DEBUG . " */\n" + ,'minifier' => '' + ))); + } $this->sources = $sources; } else { $this->log("No sources to serve"); } return $options; } + + /** + * @param string $file + * + * @param array $cOptions + * + * @return Minify_Source + */ + 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 + * + * @param string $sourceOrExt + * + * @throws Exception + */ + 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/lib/minify/lib/Minify/Controller/Page.php b/lib/minify/lib/Minify/Controller/Page.php index fa4599abd9e..de471e1248e 100644 --- a/lib/minify/lib/Minify/Controller/Page.php +++ b/lib/minify/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/lib/minify/lib/Minify/Controller/Version1.php b/lib/minify/lib/Minify/Controller/Version1.php index 1861aabc11e..5279d36dd1a 100644 --- a/lib/minify/lib/Minify/Controller/Version1.php +++ b/lib/minify/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/lib/minify/lib/Minify/DebugDetector.php b/lib/minify/lib/Minify/DebugDetector.php new file mode 100644 index 00000000000..1def97484fc --- /dev/null +++ b/lib/minify/lib/Minify/DebugDetector.php @@ -0,0 +1,26 @@ + + */ +class Minify_DebugDetector { + public static function shouldDebugRequest($cookie, $get, $requestUri) + { + if (isset($get['debug'])) { + return true; + } + if (! empty($cookie['minifyDebug'])) { + foreach (preg_split('/\\s+/', $cookie['minifyDebug']) as $debugUri) { + $pattern = '@' . preg_quote($debugUri, '@') . '@i'; + $pattern = str_replace(array('\\*', '\\?'), array('.*', '.'), $pattern); + if (preg_match($pattern, $requestUri)) { + return true; + } + } + } + return false; + } +} diff --git a/lib/minify/lib/Minify/HTML.php b/lib/minify/lib/Minify/HTML.php index fb5c1e98291..e9453ffb419 100644 --- a/lib/minify/lib/Minify/HTML.php +++ b/lib/minify/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, '_replacementHash = 'MINIFYHTML' . md5($_SERVER['REQUEST_TIME']); - $this->_placeholders = array(); - - // replace SCRIPTs (and minify) with placeholders - $this->_html = preg_replace_callback( - '/(\\s*)(]*?>)([\\s\\S]*?)<\\/script>(\\s*)/i' - ,array($this, '_removeScriptCB') - ,$this->_html); - - // replace STYLEs (and minify) with placeholders - $this->_html = preg_replace_callback( - '/\\s*(]*?>)([\\s\\S]*?)<\\/style>\\s*/i' - ,array($this, '_removeStyleCB') - ,$this->_html); - - // remove HTML comments (not containing IE conditional comments). - $this->_html = preg_replace_callback( - '//' - ,array($this, '_commentCB') - ,$this->_html); - - // replace PREs with placeholders - $this->_html = preg_replace_callback('/\\s*(]*?>[\\s\\S]*?<\\/pre>)\\s*/i' - ,array($this, '_removePreCB') - ,$this->_html); - - // replace TEXTAREAs with placeholders - $this->_html = preg_replace_callback( - '/\\s*(]*?>[\\s\\S]*?<\\/textarea>)\\s*/i' - ,array($this, '_removeTextareaCB') - ,$this->_html); - - // trim each line. - // @todo take into account attribute values that span multiple lines. - $this->_html = preg_replace('/^\\s+|\\s+$/m', '', $this->_html); - - // remove ws around block/undisplayed elements - $this->_html = preg_replace('/\\s+(<\\/?(?:area|base(?:font)?|blockquote|body' - .'|caption|center|cite|col(?:group)?|dd|dir|div|dl|dt|fieldset|form' - .'|frame(?:set)?|h[1-6]|head|hr|html|legend|li|link|map|menu|meta' - .'|ol|opt(?:group|ion)|p|param|t(?:able|body|head|d|h||r|foot|itle)' - .'|ul)\\b[^>]*>)/i', '$1', $this->_html); - - // remove ws outside of all elements - $this->_html = preg_replace_callback( - '/>([^<]+)_html); - - // use newlines before 1st attribute in open tags (to limit line lengths) - $this->_html = preg_replace('/(<[a-z\\-]+)\\s+([^>]+>)/i', "$1\n$2", $this->_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, '_replacementHash = 'MINIFYHTML' . md5($_SERVER['REQUEST_TIME']); + $this->_placeholders = array(); + + // replace SCRIPTs (and minify) with placeholders + $this->_html = preg_replace_callback( + '/(\\s*)]*?>)([\\s\\S]*?)<\\/script>(\\s*)/i' + ,array($this, '_removeScriptCB') + ,$this->_html); + + // replace STYLEs (and minify) with placeholders + $this->_html = preg_replace_callback( + '/\\s*]*>)([\\s\\S]*?)<\\/style>\\s*/i' + ,array($this, '_removeStyleCB') + ,$this->_html); + + // remove HTML comments (not containing IE conditional comments). + $this->_html = preg_replace_callback( + '//' + ,array($this, '_commentCB') + ,$this->_html); + + // replace PREs with placeholders + $this->_html = preg_replace_callback('/\\s*]*?>[\\s\\S]*?<\\/pre>)\\s*/i' + ,array($this, '_removePreCB') + ,$this->_html); + + // replace TEXTAREAs with placeholders + $this->_html = preg_replace_callback( + '/\\s*]*?>[\\s\\S]*?<\\/textarea>)\\s*/i' + ,array($this, '_removeTextareaCB') + ,$this->_html); + + // trim each line. + // @todo take into account attribute values that span multiple lines. + $this->_html = preg_replace('/^\\s+|\\s+$/m', '', $this->_html); + + // remove ws around block/undisplayed elements + $this->_html = preg_replace('/\\s+(<\\/?(?:area|base(?:font)?|blockquote|body' + .'|caption|center|cite|col(?:group)?|dd|dir|div|dl|dt|fieldset|form' + .'|frame(?:set)?|h[1-6]|head|hr|html|legend|li|link|map|menu|meta' + .'|ol|opt(?:group|ion)|p|param|t(?:able|body|head|d|h||r|foot|itle)' + .'|ul)\\b[^>]*>)/i', '$1', $this->_html); + + // remove ws outside of all elements + $this->_html = preg_replace( + '/>(\\s(?:\\s*))?([^<]+)(\\s(?:\s*))?$1$2$3<' + ,$this->_html); + + // use newlines before 1st attribute in open tags (to limit line lengths) + $this->_html = preg_replace('/(<[a-z\\-]+)\\s+([^>]+>)/i', "$1\n$2", $this->_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("\\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 = "\\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)); + } +} diff --git a/lib/minify/lib/Minify/HTML/Helper.php b/lib/minify/lib/Minify/HTML/Helper.php new file mode 100644 index 00000000000..807fdc9c197 --- /dev/null +++ b/lib/minify/lib/Minify/HTML/Helper.php @@ -0,0 +1,193 @@ + + */ +class Minify_HTML_Helper { + public $rewriteWorks = true; + public $minAppUri = '/min'; + public $groupsConfigFile = ''; + + /* + * Get an HTML-escaped Minify URI for a group or set of files + * + * @param mixed $keyOrFiles a group key or array of filepaths/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 + */ + public static function getUri($keyOrFiles, $opts = array()) + { + $opts = array_merge(array( // default options + 'farExpires' => true + ,'debug' => false + ,'charset' => 'UTF-8' + ,'minAppUri' => '/min' + ,'rewriteWorks' => true + ,'groupsConfigFile' => '' + ), $opts); + $h = new self; + $h->minAppUri = $opts['minAppUri']; + $h->rewriteWorks = $opts['rewriteWorks']; + $h->groupsConfigFile = $opts['groupsConfigFile']; + if (is_array($keyOrFiles)) { + $h->setFiles($keyOrFiles, $opts['farExpires']); + } else { + $h->setGroup($keyOrFiles, $opts['farExpires']); + } + $uri = $h->getRawUri($opts['farExpires'], $opts['debug']); + return htmlspecialchars($uri, ENT_QUOTES, $opts['charset']); + } + + /* + * Get non-HTML-escaped URI to minify the specified files + */ + public function getRawUri($farExpires = true, $debug = false) + { + $path = rtrim($this->minAppUri, '/') . '/'; + if (! $this->rewriteWorks) { + $path .= '?'; + } + if (null === $this->_groupKey) { + // @todo: implement shortest uri + $path = self::_getShortestUri($this->_filePaths, $path); + } else { + $path .= "g=" . $this->_groupKey; + } + if ($debug) { + $path .= "&debug"; + } elseif ($farExpires && $this->_lastModified) { + $path .= "&" . $this->_lastModified; + } + return $path; + } + + public function setFiles($files, $checkLastModified = true) + { + $this->_groupKey = null; + if ($checkLastModified) { + $this->_lastModified = self::getLastModified($files); + } + // normalize paths like in /min/f= + foreach ($files as $k => $file) { + if (0 === strpos($file, '//')) { + $file = substr($file, 2); + } elseif (0 === strpos($file, '/') + || 1 === strpos($file, ':\\')) { + $file = substr($file, strlen($_SERVER['DOCUMENT_ROOT']) + 1); + } + $file = strtr($file, '\\', '/'); + $files[$k] = $file; + } + $this->_filePaths = $files; + } + + public function setGroup($key, $checkLastModified = true) + { + $this->_groupKey = $key; + if ($checkLastModified) { + if (! $this->groupsConfigFile) { + $this->groupsConfigFile = dirname(dirname(dirname(dirname(__FILE__)))) . '/groupsConfig.php'; + } + if (is_file($this->groupsConfigFile)) { + $gc = (require $this->groupsConfigFile); + if (isset($gc[$key])) { + $this->_lastModified = self::getLastModified($gc[$key]); + } + } + } + } + + public static function getLastModified($sources, $lastModified = 0) + { + $max = $lastModified; + foreach ((array)$sources as $source) { + if (is_object($source) && isset($source->lastModified)) { + $max = max($max, $source->lastModified); + } elseif (is_string($source)) { + if (0 === strpos($source, '//')) { + $source = $_SERVER['DOCUMENT_ROOT'] . substr($source, 1); + } + if (is_file($source)) { + $max = max($max, filemtime($source)); + } + } + } + return $max; + } + + protected $_groupKey = null; // if present, URI will be like g=... + protected $_filePaths = array(); + protected $_lastModified = null; + + + /** + * In a given array of strings, find the character they all have at + * a particular index + * + * @param array $arr array of strings + * @param int $pos index to check + * @return mixed a common char or '' if any do not match + */ + protected static function _getCommonCharAtPos($arr, $pos) { + $l = count($arr); + $c = $arr[0][$pos]; + if ($c === '' || $l === 1) + return $c; + for ($i = 1; $i < $l; ++$i) + if ($arr[$i][$pos] !== $c) + return ''; + return $c; + } + + /** + * Get the shortest URI to minify the set of source files + * + * @param array $paths root-relative URIs of files + * @param string $minRoot root-relative URI of the "min" application + */ + protected static function _getShortestUri($paths, $minRoot = '/min/') { + $pos = 0; + $base = ''; + $c; + while (true) { + $c = self::_getCommonCharAtPos($paths, $pos); + if ($c === '') { + break; + } else { + $base .= $c; + } + ++$pos; + } + $base = preg_replace('@[^/]+$@', '', $base); + $uri = $minRoot . 'f=' . implode(',', $paths); + + if (substr($base, -1) === '/') { + // we have a base dir! + $basedPaths = $paths; + $l = count($paths); + for ($i = 0; $i < $l; ++$i) { + $basedPaths[$i] = substr($paths[$i], strlen($base)); + } + $base = substr($base, 0, strlen($base) - 1); + $bUri = $minRoot . 'b=' . $base . '&f=' . implode(',', $basedPaths); + + $uri = strlen($uri) < strlen($bUri) + ? $uri + : $bUri; + } + return $uri; + } +} diff --git a/lib/minify/lib/Minify/ImportProcessor.php b/lib/minify/lib/Minify/ImportProcessor.php index 0d6d90a8166..bdfae547d7b 100644 --- a/lib/minify/lib/Minify/ImportProcessor.php +++ b/lib/minify/lib/Minify/ImportProcessor.php @@ -1,157 +1,216 @@ - - */ -class Minify_ImportProcessor { - - public static $filesIncluded = array(); - - public static function process($file) - { - self::$filesIncluded = array(); - self::$_isCss = (strtolower(substr($file, -4)) === '.css'); - $obj = new Minify_ImportProcessor(dirname($file)); - return $obj->_getContent($file); - } - - // allows callback funcs to know the current directory - private $_currentDir = null; - - // allows _importCB to write the fetched content back to the obj - private $_importedContent = ''; - - private static $_isCss = null; - - private function __construct($currentDir) - { - $this->_currentDir = $currentDir; - } - - private function _getContent($file) - { - $file = realpath($file); - if (! $file - || in_array($file, self::$filesIncluded) - || false === ($content = @file_get_contents($file)) - ) { - // file missing, already included, or failed read - return ''; - } - self::$filesIncluded[] = realpath($file); - $this->_currentDir = dirname($file); - - // remove UTF-8 BOM if present - if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) { - $content = substr($content, 3); - } - // ensure uniform EOLs - $content = str_replace("\r\n", "\n", $content); - - // process @imports - $content = preg_replace_callback( - '/ - @import\\s+ - (?:url\\(\\s*)? # maybe url( - [\'"]? # maybe quote - (.*?) # 1 = URI - [\'"]? # maybe end quote - (?:\\s*\\))? # maybe ) - ([a-zA-Z,\\s]*)? # 2 = media list - ; # end token - /x' - ,array($this, '_importCB') - ,$content - ); - - if (self::$_isCss) { - // rewrite remaining relative URIs - $content = preg_replace_callback( - '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' - ,array($this, '_urlCB') - ,$content - ); - } - - return $this->_importedContent . $content; - } - - private function _importCB($m) - { - $url = $m[1]; - $mediaList = preg_replace('/\\s+/', '', $m[2]); - - if (strpos($url, '://') > 0) { - // protocol, leave in place for CSS, comment for JS - return self::$_isCss - ? $m[0] - : "/* Minify_ImportProcessor will not include remote content */"; - } - if ('/' === $url[0]) { - // protocol-relative or root path - $url = ltrim($url, '/'); - $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR - . strtr($url, '/', DIRECTORY_SEPARATOR); - } else { - // relative to current path - $file = $this->_currentDir . DIRECTORY_SEPARATOR - . strtr($url, '/', DIRECTORY_SEPARATOR); - } - $obj = new Minify_ImportProcessor(dirname($file)); - $content = $obj->_getContent($file); - if ('' === $content) { - // failed. leave in place for CSS, comment for JS - return self::$_isCss - ? $m[0] - : "/* Minify_ImportProcessor could not fetch '{$file}' */";; - } - return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList)) - ? $content - : "@media {$mediaList} {\n{$content}\n}\n"; - } - - private function _urlCB($m) - { - // $m[1] is either quoted or not - $quote = ($m[1][0] === "'" || $m[1][0] === '"') - ? $m[1][0] - : ''; - $url = ($quote === '') - ? $m[1] - : substr($m[1], 1, strlen($m[1]) - 2); - if ('/' !== $url[0]) { - if (strpos($url, '//') > 0) { - // probably starts with protocol, do not alter - } else { - // prepend path with current dir separator (OS-independent) - $path = $this->_currentDir - . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR); - // strip doc root - $path = substr($path, strlen(realpath($_SERVER['DOCUMENT_ROOT']))); - // fix to absolute URL - $url = strtr($path, '/\\', '//'); - // remove /./ and /../ where possible - $url = str_replace('/./', '/', $url); - // inspired by patch from Oleg Cherniy - do { - $url = preg_replace('@/[^/]+/\\.\\./@', '/', $url, 1, $changed); - } while ($changed); - } - } - return "url({$quote}{$url}{$quote})"; - } -} + + * @author Simon Schick + */ +class Minify_ImportProcessor { + + public static $filesIncluded = array(); + + public static function process($file) + { + self::$filesIncluded = array(); + self::$_isCss = (strtolower(substr($file, -4)) === '.css'); + $obj = new Minify_ImportProcessor(dirname($file)); + return $obj->_getContent($file); + } + + // allows callback funcs to know the current directory + private $_currentDir = null; + + // allows callback funcs to know the directory of the file that inherits this one + private $_previewsDir = null; + + // allows _importCB to write the fetched content back to the obj + private $_importedContent = ''; + + private static $_isCss = null; + + /** + * @param String $currentDir + * @param String $previewsDir Is only used internally + */ + private function __construct($currentDir, $previewsDir = "") + { + $this->_currentDir = $currentDir; + $this->_previewsDir = $previewsDir; + } + + private function _getContent($file, $is_imported = false) + { + $file = realpath($file); + if (! $file + || in_array($file, self::$filesIncluded) + || false === ($content = @file_get_contents($file)) + ) { + // file missing, already included, or failed read + return ''; + } + self::$filesIncluded[] = realpath($file); + $this->_currentDir = dirname($file); + + // remove UTF-8 BOM if present + if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) { + $content = substr($content, 3); + } + // ensure uniform EOLs + $content = str_replace("\r\n", "\n", $content); + + // process @imports + $content = preg_replace_callback( + '/ + @import\\s+ + (?:url\\(\\s*)? # maybe url( + [\'"]? # maybe quote + (.*?) # 1 = URI + [\'"]? # maybe end quote + (?:\\s*\\))? # maybe ) + ([a-zA-Z,\\s]*)? # 2 = media list + ; # end token + /x' + ,array($this, '_importCB') + ,$content + ); + + // You only need to rework the import-path if the script is imported + if (self::$_isCss && $is_imported) { + // rewrite remaining relative URIs + $content = preg_replace_callback( + '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' + ,array($this, '_urlCB') + ,$content + ); + } + + return $this->_importedContent . $content; + } + + private function _importCB($m) + { + $url = $m[1]; + $mediaList = preg_replace('/\\s+/', '', $m[2]); + + if (strpos($url, '://') > 0) { + // protocol, leave in place for CSS, comment for JS + return self::$_isCss + ? $m[0] + : "/* Minify_ImportProcessor will not include remote content */"; + } + if ('/' === $url[0]) { + // protocol-relative or root path + $url = ltrim($url, '/'); + $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR + . strtr($url, '/', DIRECTORY_SEPARATOR); + } else { + // relative to current path + $file = $this->_currentDir . DIRECTORY_SEPARATOR + . strtr($url, '/', DIRECTORY_SEPARATOR); + } + $obj = new Minify_ImportProcessor(dirname($file), $this->_currentDir); + $content = $obj->_getContent($file, true); + if ('' === $content) { + // failed. leave in place for CSS, comment for JS + return self::$_isCss + ? $m[0] + : "/* Minify_ImportProcessor could not fetch '{$file}' */"; + } + return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList)) + ? $content + : "@media {$mediaList} {\n{$content}\n}\n"; + } + + private function _urlCB($m) + { + // $m[1] is either quoted or not + $quote = ($m[1][0] === "'" || $m[1][0] === '"') + ? $m[1][0] + : ''; + $url = ($quote === '') + ? $m[1] + : substr($m[1], 1, strlen($m[1]) - 2); + if ('/' !== $url[0]) { + if (strpos($url, '//') > 0) { + // probably starts with protocol, do not alter + } else { + // prepend path with current dir separator (OS-independent) + $path = $this->_currentDir + . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR); + // update the relative path by the directory of the file that imported this one + $url = self::getPathDiff(realpath($this->_previewsDir), $path); + } + } + return "url({$quote}{$url}{$quote})"; + } + + /** + * @param string $from + * @param string $to + * @param string $ps + * @return string + */ + private function getPathDiff($from, $to, $ps = DIRECTORY_SEPARATOR) + { + $realFrom = $this->truepath($from); + $realTo = $this->truepath($to); + + $arFrom = explode($ps, rtrim($realFrom, $ps)); + $arTo = explode($ps, rtrim($realTo, $ps)); + while (count($arFrom) && count($arTo) && ($arFrom[0] == $arTo[0])) + { + array_shift($arFrom); + array_shift($arTo); + } + return str_pad("", count($arFrom) * 3, '..' . $ps) . implode($ps, $arTo); + } + + /** + * This function is to replace PHP's extremely buggy realpath(). + * @param string $path The original path, can be relative etc. + * @return string The resolved path, it might not exist. + * @see http://stackoverflow.com/questions/4049856/replace-phps-realpath + */ + function truepath($path) + { + // whether $path is unix or not + $unipath = strlen($path) == 0 || $path{0} != '/'; + // attempts to detect if path is relative in which case, add cwd + if (strpos($path, ':') === false && $unipath) + $path = $this->_currentDir . DIRECTORY_SEPARATOR . $path; + + // resolve path parts (single dot, double dot and double delimiters) + $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path); + $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen'); + $absolutes = array(); + foreach ($parts as $part) { + if ('.' == $part) + continue; + if ('..' == $part) { + array_pop($absolutes); + } else { + $absolutes[] = $part; + } + } + $path = implode(DIRECTORY_SEPARATOR, $absolutes); + // resolve any symlinks + if (file_exists($path) && linkinfo($path) > 0) + $path = readlink($path); + // put initial separator that could have been lost + $path = !$unipath ? '/' . $path : $path; + return $path; + } +} diff --git a/lib/minify/lib/Minify/JS/ClosureCompiler.php b/lib/minify/lib/Minify/JS/ClosureCompiler.php new file mode 100644 index 00000000000..f4ed5bdb682 --- /dev/null +++ b/lib/minify/lib/Minify/JS/ClosureCompiler.php @@ -0,0 +1,133 @@ + + * + * @todo can use a stream wrapper to unit test this? + */ +class Minify_JS_ClosureCompiler { + const URL = 'http://closure-compiler.appspot.com/compile'; + + /** + * Minify Javascript code via HTTP request to the Closure Compiler API + * + * @param string $js input code + * @param array $options unused at this point + * @return string + */ + public static function minify($js, array $options = array()) + { + $obj = new self($options); + return $obj->min($js); + } + + /** + * + * @param array $options + * + * fallbackFunc : default array($this, 'fallback'); + */ + public function __construct(array $options = array()) + { + $this->_fallbackFunc = isset($options['fallbackMinifier']) + ? $options['fallbackMinifier'] + : array($this, '_fallback'); + } + + public function min($js) + { + $postBody = $this->_buildPostBody($js); + $bytes = (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($postBody, '8bit') + : strlen($postBody); + if ($bytes > 200000) { + throw new Minify_JS_ClosureCompiler_Exception( + 'POST content larger than 200000 bytes' + ); + } + $response = $this->_getResponse($postBody); + if (preg_match('/^Error\(\d\d?\):/', $response)) { + if (is_callable($this->_fallbackFunc)) { + $response = "/* Received errors from Closure Compiler API:\n$response" + . "\n(Using fallback minifier)\n*/\n"; + $response .= call_user_func($this->_fallbackFunc, $js); + } else { + throw new Minify_JS_ClosureCompiler_Exception($response); + } + } + if ($response === '') { + $errors = $this->_getResponse($this->_buildPostBody($js, true)); + throw new Minify_JS_ClosureCompiler_Exception($errors); + } + return $response; + } + + protected $_fallbackFunc = null; + + protected function _getResponse($postBody) + { + $allowUrlFopen = preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen')); + if ($allowUrlFopen) { + $contents = file_get_contents(self::URL, false, stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => $postBody, + 'max_redirects' => 0, + 'timeout' => 15, + ) + ))); + } elseif (defined('CURLOPT_POST')) { + $ch = curl_init(self::URL); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/x-www-form-urlencoded')); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postBody); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15); + $contents = curl_exec($ch); + curl_close($ch); + } else { + throw new Minify_JS_ClosureCompiler_Exception( + "Could not make HTTP request: allow_url_open is false and cURL not available" + ); + } + if (false === $contents) { + throw new Minify_JS_ClosureCompiler_Exception( + "No HTTP response from server" + ); + } + return trim($contents); + } + + protected function _buildPostBody($js, $returnErrors = false) + { + return http_build_query(array( + 'js_code' => $js, + 'output_info' => ($returnErrors ? 'errors' : 'compiled_code'), + 'output_format' => 'text', + 'compilation_level' => 'SIMPLE_OPTIMIZATIONS' + ), null, '&'); + } + + /** + * Default fallback function if CC API fails + * @param string $js + * @return string + */ + protected function _fallback($js) + { + require_once 'JSMin.php'; + return JSMin::minify($js); + } +} + +class Minify_JS_ClosureCompiler_Exception extends Exception {} diff --git a/lib/minify/lib/Minify/Lines.php b/lib/minify/lib/Minify/Lines.php index 186e7019d4c..ca8afa1bc97 100644 --- a/lib/minify/lib/Minify/Lines.php +++ b/lib/minify/lib/Minify/Lines.php @@ -40,10 +40,16 @@ class Minify_Lines { ? $options['id'] : ''; $content = str_replace("\r\n", "\n", $content); + + // Hackily rewrite strings with XPath expressions that are + // likely to throw off our dumb parser (for Prototype 1.6.1). + $content = str_replace('"/*"', '"/"+"*"', $content); + $content = preg_replace('@([\'"])(\\.?//?)\\*@', '$1$2$1+$1*', $content); + $lines = explode("\n", $content); $numLines = count($lines); // determine left padding - $padTo = strlen($numLines); + $padTo = strlen((string) $numLines); // e.g. 103 lines = 3 digits $inComment = false; $i = 0; $newLines = array(); @@ -82,7 +88,7 @@ class Minify_Lines { * * @param bool $inComment was the parser in a comment at the * beginning of the line? - * + * * @return bool */ private static function _eolInComment($line, $inComment) diff --git a/lib/minify/lib/Minify/Logger.php b/lib/minify/lib/Minify/Logger.php index 7844eea3566..8eb72f45289 100644 --- a/lib/minify/lib/Minify/Logger.php +++ b/lib/minify/lib/Minify/Logger.php @@ -9,6 +9,8 @@ * * @package Minify * @author Stephen Clay + * + * @todo lose this singleton! pass log object in Minify::serve and distribute to others */ class Minify_Logger { diff --git a/lib/minify/lib/Minify/YUI/CssCompressor.java b/lib/minify/lib/Minify/YUI/CssCompressor.java new file mode 100644 index 00000000000..3fb497a9697 --- /dev/null +++ b/lib/minify/lib/Minify/YUI/CssCompressor.java @@ -0,0 +1,382 @@ +/* + * YUI Compressor + * http://developer.yahoo.com/yui/compressor/ + * Author: Julien Lecomte - http://www.julienlecomte.net/ + * Author: Isaac Schlueter - http://foohack.com/ + * Author: Stoyan Stefanov - http://phpied.com/ + * Copyright (c) 2011 Yahoo! Inc. All rights reserved. + * The copyrights embodied in the content of this file are licensed + * by Yahoo! Inc. under the BSD (revised) open source license. + */ +package com.yahoo.platform.yui.compressor; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import java.util.ArrayList; + +public class CssCompressor { + + private StringBuffer srcsb = new StringBuffer(); + + public CssCompressor(Reader in) throws IOException { + // Read the stream... + int c; + while ((c = in.read()) != -1) { + srcsb.append((char) c); + } + } + + // Leave data urls alone to increase parse performance. + protected String extractDataUrls(String css, ArrayList preservedTokens) { + + int maxIndex = css.length() - 1; + int appendIndex = 0; + + StringBuffer sb = new StringBuffer(); + + Pattern p = Pattern.compile("url\\(\\s*([\"']?)data\\:"); + Matcher m = p.matcher(css); + + /* + * Since we need to account for non-base64 data urls, we need to handle + * ' and ) being part of the data string. Hence switching to indexOf, + * to determine whether or not we have matching string terminators and + * handling sb appends directly, instead of using matcher.append* methods. + */ + + while (m.find()) { + + int startIndex = m.start() + 4; // "url(".length() + String terminator = m.group(1); // ', " or empty (not quoted) + + if (terminator.length() == 0) { + terminator = ")"; + } + + boolean foundTerminator = false; + + int endIndex = m.end() - 1; + while(foundTerminator == false && endIndex+1 <= maxIndex) { + endIndex = css.indexOf(terminator, endIndex+1); + + if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) { + foundTerminator = true; + if (!")".equals(terminator)) { + endIndex = css.indexOf(")", endIndex); + } + } + } + + // Enough searching, start moving stuff over to the buffer + sb.append(css.substring(appendIndex, m.start())); + + if (foundTerminator) { + String token = css.substring(startIndex, endIndex); + token = token.replaceAll("\\s+", ""); + preservedTokens.add(token); + + String preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)"; + sb.append(preserver); + + appendIndex = endIndex + 1; + } else { + // No end terminator found, re-add the whole match. Should we throw/warn here? + sb.append(css.substring(m.start(), m.end())); + appendIndex = m.end(); + } + } + + sb.append(css.substring(appendIndex)); + + return sb.toString(); + } + + public void compress(Writer out, int linebreakpos) + throws IOException { + + Pattern p; + Matcher m; + String css = srcsb.toString(); + + int startIndex = 0; + int endIndex = 0; + int i = 0; + int max = 0; + ArrayList preservedTokens = new ArrayList(0); + ArrayList comments = new ArrayList(0); + String token; + int totallen = css.length(); + String placeholder; + + css = this.extractDataUrls(css, preservedTokens); + + StringBuffer sb = new StringBuffer(css); + + // collect all comment blocks... + while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) { + endIndex = sb.indexOf("*/", startIndex + 2); + if (endIndex < 0) { + endIndex = totallen; + } + + token = sb.substring(startIndex + 2, endIndex); + comments.add(token); + sb.replace(startIndex + 2, endIndex, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1) + "___"); + startIndex += 2; + } + css = sb.toString(); + + // preserve strings so their content doesn't get accidentally minified + sb = new StringBuffer(); + p = Pattern.compile("(\"([^\\\\\"]|\\\\.|\\\\)*\")|(\'([^\\\\\']|\\\\.|\\\\)*\')"); + m = p.matcher(css); + while (m.find()) { + token = m.group(); + char quote = token.charAt(0); + token = token.substring(1, token.length() - 1); + + // maybe the string contains a comment-like substring? + // one, maybe more? put'em back then + if (token.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) { + for (i = 0, max = comments.size(); i < max; i += 1) { + token = token.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments.get(i).toString()); + } + } + + // minify alpha opacity in filter strings + token = token.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity="); + + preservedTokens.add(token); + String preserver = quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___" + quote; + m.appendReplacement(sb, preserver); + } + m.appendTail(sb); + css = sb.toString(); + + + // strings are safe, now wrestle the comments + for (i = 0, max = comments.size(); i < max; i += 1) { + + token = comments.get(i).toString(); + placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___"; + + // ! in the first position of the comment means preserve + // so push to the preserved tokens while stripping the ! + if (token.startsWith("!")) { + preservedTokens.add(token); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + continue; + } + + // \ in the last position looks like hack for Mac/IE5 + // shorten that to /*\*/ and the next one to /**/ + if (token.endsWith("\\")) { + preservedTokens.add("\\"); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + i = i + 1; // attn: advancing the loop + preservedTokens.add(""); + css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + continue; + } + + // keep empty comments after child selectors (IE7 hack) + // e.g. html >/**/ body + if (token.length() == 0) { + startIndex = css.indexOf(placeholder); + if (startIndex > 2) { + if (css.charAt(startIndex - 3) == '>') { + preservedTokens.add(""); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + } + } + } + + // in all other cases kill the comment + css = css.replace("/*" + placeholder + "*/", ""); + } + + + // Normalize all whitespace strings to single spaces. Easier to work with that way. + css = css.replaceAll("\\s+", " "); + + // Remove the spaces before the things that should not have spaces before them. + // But, be careful not to turn "p :link {...}" into "p:link{...}" + // Swap out any pseudo-class colons with the token, and then swap back. + sb = new StringBuffer(); + p = Pattern.compile("(^|\\})(([^\\{:])+:)+([^\\{]*\\{)"); + m = p.matcher(css); + while (m.find()) { + String s = m.group(); + s = s.replaceAll(":", "___YUICSSMIN_PSEUDOCLASSCOLON___"); + s = s.replaceAll( "\\\\", "\\\\\\\\" ).replaceAll( "\\$", "\\\\\\$" ); + m.appendReplacement(sb, s); + } + m.appendTail(sb); + css = sb.toString(); + // Remove spaces before the things that should not have spaces before them. + css = css.replaceAll("\\s+([!{};:>+\\(\\)\\],])", "$1"); + // bring back the colon + css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":"); + + // retain space for special IE6 cases + css = css.replaceAll(":first\\-(line|letter)(\\{|,)", ":first-$1 $2"); + + // no space after the end of a preserved comment + css = css.replaceAll("\\*/ ", "*/"); + + // If there is a @charset, then only allow one, and push to the top of the file. + css = css.replaceAll("^(.*)(@charset \"[^\"]*\";)", "$2$1"); + css = css.replaceAll("^(\\s*@charset [^;]+;\\s*)+", "$1"); + + // Put the space back in some cases, to support stuff like + // @media screen and (-webkit-min-device-pixel-ratio:0){ + css = css.replaceAll("\\band\\(", "and ("); + + // Remove the spaces after the things that should not have spaces after them. + css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1"); + + // remove unnecessary semicolons + css = css.replaceAll(";+}", "}"); + + // Replace 0(px,em,%) with 0. + css = css.replaceAll("([\\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", "$1$2"); + + // Replace 0 0 0 0; with 0. + css = css.replaceAll(":0 0 0 0(;|})", ":0$1"); + css = css.replaceAll(":0 0 0(;|})", ":0$1"); + css = css.replaceAll(":0 0(;|})", ":0$1"); + + + // Replace background-position:0; with background-position:0 0; + // same for transform-origin + sb = new StringBuffer(); + p = Pattern.compile("(?i)(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|})"); + m = p.matcher(css); + while (m.find()) { + m.appendReplacement(sb, m.group(1).toLowerCase() + ":0 0" + m.group(2)); + } + m.appendTail(sb); + css = sb.toString(); + + // Replace 0.6 to .6, but only when preceded by : or a white-space + css = css.replaceAll("(:|\\s)0+\\.(\\d+)", "$1.$2"); + + // Shorten colors from rgb(51,102,153) to #336699 + // This makes it more likely that it'll get further compressed in the next step. + p = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)"); + m = p.matcher(css); + sb = new StringBuffer(); + while (m.find()) { + String[] rgbcolors = m.group(1).split(","); + StringBuffer hexcolor = new StringBuffer("#"); + for (i = 0; i < rgbcolors.length; i++) { + int val = Integer.parseInt(rgbcolors[i]); + if (val < 16) { + hexcolor.append("0"); + } + hexcolor.append(Integer.toHexString(val)); + } + m.appendReplacement(sb, hexcolor.toString()); + } + m.appendTail(sb); + css = sb.toString(); + + // Shorten colors from #AABBCC to #ABC. Note that we want to make sure + // the color is not preceded by either ", " or =. Indeed, the property + // filter: chroma(color="#FFFFFF"); + // would become + // filter: chroma(color="#FFF"); + // which makes the filter break in IE. + // We also want to make sure we're only compressing #AABBCC patterns inside { }, not id selectors ( #FAABAC {} ) + // We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD) + p = Pattern.compile("(\\=\\s*?[\"']?)?" + "#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])" + "(:?\\}|[^0-9a-fA-F{][^{]*?\\})"); + + m = p.matcher(css); + sb = new StringBuffer(); + int index = 0; + + while (m.find(index)) { + + sb.append(css.substring(index, m.start())); + + boolean isFilter = (m.group(1) != null && !"".equals(m.group(1))); + + if (isFilter) { + // Restore, as is. Compression will break filters + sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)); + } else { + if( m.group(2).equalsIgnoreCase(m.group(3)) && + m.group(4).equalsIgnoreCase(m.group(5)) && + m.group(6).equalsIgnoreCase(m.group(7))) { + + // #AABBCC pattern + sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase()); + + } else { + + // Non-compressible color, restore, but lower case. + sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase()); + } + } + + index = m.end(7); + } + + sb.append(css.substring(index)); + css = sb.toString(); + + // border: none -> border:0 + sb = new StringBuffer(); + p = Pattern.compile("(?i)(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|})"); + m = p.matcher(css); + while (m.find()) { + m.appendReplacement(sb, m.group(1).toLowerCase() + ":0" + m.group(2)); + } + m.appendTail(sb); + css = sb.toString(); + + // shorter opacity IE filter + css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity="); + + // Remove empty rules. + css = css.replaceAll("[^\\}\\{/;]+\\{\\}", ""); + + // TODO: Should this be after we re-insert tokens. These could alter the break points. However then + // we'd need to make sure we don't break in the middle of a string etc. + if (linebreakpos >= 0) { + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + i = 0; + int linestartpos = 0; + sb = new StringBuffer(css); + while (i < sb.length()) { + char c = sb.charAt(i++); + if (c == '}' && i - linestartpos > linebreakpos) { + sb.insert(i, '\n'); + linestartpos = i; + } + } + + css = sb.toString(); + } + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + css = css.replaceAll(";;+", ";"); + + // restore preserved comments and strings + for(i = 0, max = preservedTokens.size(); i < max; i++) { + css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens.get(i).toString()); + } + + // Trim the final string (for any leading or trailing white spaces) + css = css.trim(); + + // Write the output... + out.write(css); + } +} diff --git a/lib/minify/lib/Minify/YUI/CssCompressor.php b/lib/minify/lib/Minify/YUI/CssCompressor.php new file mode 100644 index 00000000000..ae3443d4459 --- /dev/null +++ b/lib/minify/lib/Minify/YUI/CssCompressor.php @@ -0,0 +1,171 @@ ++\\(\\)\\],])@", "$1", $css); + $css = str_replace("___PSEUDOCLASSCOLON___", ":", $css); + + // Remove the spaces after the things that should not have spaces after them. + $css = preg_replace("@([!{}:;>+\\(\\[,])\\s+@", "$1", $css); + + // Add the semicolon where it's missing. + $css = preg_replace("@([^;\\}])}@", "$1;}", $css); + + // Replace 0(px,em,%) with 0. + $css = preg_replace("@([\\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)@", "$1$2", $css); + + // Replace 0 0 0 0; with 0. + $css = str_replace(":0 0 0 0;", ":0;", $css); + $css = str_replace(":0 0 0;", ":0;", $css); + $css = str_replace(":0 0;", ":0;", $css); + + // Replace background-position:0; with background-position:0 0; + $css = str_replace("background-position:0;", "background-position:0 0;", $css); + + // Replace 0.6 to .6, but only when preceded by : or a white-space + $css = preg_replace("@(:|\\s)0+\\.(\\d+)@", "$1.$2", $css); + + // Shorten colors from rgb(51,102,153) to #336699 + // This makes it more likely that it'll get further compressed in the next step. + $css = preg_replace_callback("@rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)@", array($this, '_shortenRgbCB'), $css); + + // Shorten colors from #AABBCC to #ABC. Note that we want to make sure + // the color is not preceded by either ", " or =. Indeed, the property + // filter: chroma(color="#FFFFFF"); + // would become + // filter: chroma(color="#FFF"); + // which makes the filter break in IE. + $css = preg_replace_callback("@([^\"'=\\s])(\\s*)#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])@", array($this, '_shortenHexCB'), $css); + + // Remove empty rules. + $css = preg_replace("@[^\\}]+\\{;\\}@", "", $css); + + $linebreakpos = isset($this->_options['linebreakpos']) + ? $this->_options['linebreakpos'] + : 0; + + if ($linebreakpos > 0) { + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + $i = 0; + $linestartpos = 0; + $sb = $css; + + // make sure strlen returns byte count + $mbIntEnc = null; + if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) { + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('8bit'); + } + $sbLength = strlen($css); + while ($i < $sbLength) { + $c = $sb[$i++]; + if ($c === '}' && $i - $linestartpos > $linebreakpos) { + $sb = substr_replace($sb, "\n", $i, 0); + $sbLength++; + $linestartpos = $i; + } + } + $css = $sb; + + // undo potential mb_encoding change + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } + } + + // Replace the pseudo class for the Box Model Hack + $css = str_replace("___PSEUDOCLASSBMH___", "\"\\\\\"}\\\\\"\"", $css); + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + $css = preg_replace("@;;+@", ";", $css); + + // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/ + $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css); + + // Trim the final string (for any leading or trailing white spaces) + $css = trim($css); + + return $css; + } + + protected function _removeSpacesCB($m) + { + return str_replace(':', '___PSEUDOCLASSCOLON___', $m[0]); + } + + protected function _shortenRgbCB($m) + { + $rgbcolors = explode(',', $m[1]); + $hexcolor = '#'; + for ($i = 0; $i < count($rgbcolors); $i++) { + $val = round($rgbcolors[$i]); + if ($val < 16) { + $hexcolor .= '0'; + } + $hexcolor .= dechex($val); + } + return $hexcolor; + } + + protected function _shortenHexCB($m) + { + // Test for AABBCC pattern + if ((strtolower($m[3])===strtolower($m[4])) && + (strtolower($m[5])===strtolower($m[6])) && + (strtolower($m[7])===strtolower($m[8]))) { + return $m[1] . $m[2] . "#" . $m[3] . $m[5] . $m[7]; + } else { + return $m[0]; + } + } +} \ No newline at end of file diff --git a/lib/minify/lib/Minify/YUICompressor.php b/lib/minify/lib/Minify/YUICompressor.php index 7cb61adbec5..c5bd8a1eea9 100644 --- a/lib/minify/lib/Minify/YUICompressor.php +++ b/lib/minify/lib/Minify/YUICompressor.php @@ -90,8 +90,11 @@ class Minify_YUICompressor { throw new Exception('Minify_YUICompressor : could not create temp file.'); } file_put_contents($tmpFile, $content); - exec(self::_getCmd($options, $type, $tmpFile), $output); + exec(self::_getCmd($options, $type, $tmpFile), $output, $result_code); unlink($tmpFile); + if ($result_code != 0) { + throw new Exception('Minify_YUICompressor : YUI compressor execution failed.'); + } return implode("\n", $output); } @@ -110,7 +113,7 @@ class Minify_YUICompressor { ); $cmd = self::$javaExecutable . ' -jar ' . escapeshellarg(self::$jarFile) . " --type {$type}" - . (preg_match('/^[a-zA-Z\\-]+$/', $o['charset']) + . (preg_match('/^[\\da-zA-Z0-9\\-]+$/', $o['charset']) ? " --charset {$o['charset']}" : '') . (is_numeric($o['line-break']) && $o['line-break'] >= 0 @@ -128,11 +131,17 @@ class Minify_YUICompressor { private static function _prepare() { - if (! is_file(self::$jarFile) - || ! is_dir(self::$tempDir) - || ! is_writable(self::$tempDir) - ) { - throw new Exception('Minify_YUICompressor : $jarFile and $tempDir must be set.'); + if (! is_file(self::$jarFile)) { + throw new Exception('Minify_YUICompressor : $jarFile('.self::$jarFile.') is not a valid file.'); + } + if (! is_executable(self::$jarFile)) { + throw new Exception('Minify_YUICompressor : $jarFile('.self::$jarFile.') is not executable.'); + } + if (! is_dir(self::$tempDir)) { + throw new Exception('Minify_YUICompressor : $tempDir('.self::$tempDir.') is not a valid direcotry.'); + } + if (! is_writable(self::$tempDir)) { + throw new Exception('Minify_YUICompressor : $tempDir('.self::$tempDir.') is not writable.'); } } } diff --git a/lib/minify/lib/MrClay/Cli.php b/lib/minify/lib/MrClay/Cli.php new file mode 100644 index 00000000000..ac201582e4f --- /dev/null +++ b/lib/minify/lib/MrClay/Cli.php @@ -0,0 +1,381 @@ +values. + * + * You may also specify that some arguments be used to provide input/output. By communicating + * solely through the file pointers provided by openInput()/openOutput(), you can make your + * app more flexible to end users. + * + * @author Steve Clay + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Cli { + + /** + * @var array validation errors + */ + public $errors = array(); + + /** + * @var array option values available after validation. + * + * E.g. array( + * 'a' => false // option was missing + * ,'b' => true // option was present + * ,'c' => "Hello" // option had value + * ,'f' => "/home/user/file" // file path from root + * ,'f.raw' => "~/file" // file path as given to option + * ) + */ + public $values = array(); + + /** + * @var array + */ + public $moreArgs = array(); + + /** + * @var array + */ + public $debug = array(); + + /** + * @var bool The user wants help info + */ + public $isHelpRequest = false; + + /** + * @var array of Cli\Arg + */ + protected $_args = array(); + + /** + * @var resource + */ + protected $_stdin = null; + + /** + * @var resource + */ + protected $_stdout = null; + + /** + * @param bool $exitIfNoStdin (default true) Exit() if STDIN is not defined + */ + public function __construct($exitIfNoStdin = true) + { + if ($exitIfNoStdin && ! defined('STDIN')) { + exit('This script is for command-line use only.'); + } + if (isset($GLOBALS['argv'][1]) + && ($GLOBALS['argv'][1] === '-?' || $GLOBALS['argv'][1] === '--help')) { + $this->isHelpRequest = true; + } + } + + /** + * @param Cli\Arg|string $letter + * @return Cli\Arg + */ + public function addOptionalArg($letter) + { + return $this->addArgument($letter, false); + } + + /** + * @param Cli\Arg|string $letter + * @return Cli\Arg + */ + public function addRequiredArg($letter) + { + return $this->addArgument($letter, true); + } + + /** + * @param string $letter + * @param bool $required + * @param Cli\Arg|null $arg + * @return Cli\Arg + * @throws \InvalidArgumentException + */ + public function addArgument($letter, $required, Cli\Arg $arg = null) + { + if (! preg_match('/^[a-zA-Z]$/', $letter)) { + throw new \InvalidArgumentException('$letter must be in [a-zA-z]'); + } + if (! $arg) { + $arg = new Cli\Arg($required); + } + $this->_args[$letter] = $arg; + return $arg; + } + + /** + * @param string $letter + * @return Cli\Arg|null + */ + public function getArgument($letter) + { + return isset($this->_args[$letter]) ? $this->_args[$letter] : null; + } + + /* + * Read and validate options + * + * @return bool true if all options are valid + */ + public function validate() + { + $options = ''; + $this->errors = array(); + $this->values = array(); + $this->_stdin = null; + + if ($this->isHelpRequest) { + return false; + } + + $lettersUsed = ''; + foreach ($this->_args as $letter => $arg) { + /* @var Cli\Arg $arg */ + $options .= $letter; + $lettersUsed .= $letter; + + if ($arg->mayHaveValue || $arg->mustHaveValue) { + $options .= ($arg->mustHaveValue ? ':' : '::'); + } + } + + $this->debug['argv'] = $GLOBALS['argv']; + $argvCopy = array_slice($GLOBALS['argv'], 1); + $o = getopt($options); + $this->debug['getopt_options'] = $options; + $this->debug['getopt_return'] = $o; + + foreach ($this->_args as $letter => $arg) { + /* @var Cli\Arg $arg */ + $this->values[$letter] = false; + if (isset($o[$letter])) { + if (is_bool($o[$letter])) { + + // remove from argv copy + $k = array_search("-$letter", $argvCopy); + if ($k !== false) { + array_splice($argvCopy, $k, 1); + } + + if ($arg->mustHaveValue) { + $this->addError($letter, "Missing value"); + } else { + $this->values[$letter] = true; + } + } else { + // string + $this->values[$letter] = $o[$letter]; + $v =& $this->values[$letter]; + + // remove from argv copy + // first look for -ovalue or -o=value + $pattern = "/^-{$letter}=?" . preg_quote($v, '/') . "$/"; + $foundInArgv = false; + foreach ($argvCopy as $k => $argV) { + if (preg_match($pattern, $argV)) { + array_splice($argvCopy, $k, 1); + $foundInArgv = true; + break; + } + } + if (! $foundInArgv) { + // space separated + $k = array_search("-$letter", $argvCopy); + if ($k !== false) { + array_splice($argvCopy, $k, 2); + } + } + + // check that value isn't really another option + if (strlen($lettersUsed) > 1) { + $pattern = "/^-[" . str_replace($letter, '', $lettersUsed) . "]/i"; + if (preg_match($pattern, $v)) { + $this->addError($letter, "Value was read as another option: %s", $v); + return false; + } + } + if ($arg->assertFile || $arg->assertDir) { + if ($v[0] !== '/' && $v[0] !== '~') { + $this->values["$letter.raw"] = $v; + $v = getcwd() . "/$v"; + } + } + if ($arg->assertFile) { + if ($arg->useAsInfile) { + $this->_stdin = $v; + } elseif ($arg->useAsOutfile) { + $this->_stdout = $v; + } + if ($arg->assertReadable && ! is_readable($v)) { + $this->addError($letter, "File not readable: %s", $v); + continue; + } + if ($arg->assertWritable) { + if (is_file($v)) { + if (! is_writable($v)) { + $this->addError($letter, "File not writable: %s", $v); + } + } else { + if (! is_writable(dirname($v))) { + $this->addError($letter, "Directory not writable: %s", dirname($v)); + } + } + } + } elseif ($arg->assertDir && $arg->assertWritable && ! is_writable($v)) { + $this->addError($letter, "Directory not readable: %s", $v); + } + } + } else { + if ($arg->isRequired()) { + $this->addError($letter, "Missing"); + } + } + } + $this->moreArgs = $argvCopy; + reset($this->moreArgs); + return empty($this->errors); + } + + /** + * Get the full paths of file(s) passed in as unspecified arguments + * + * @return array + */ + public function getPathArgs() + { + $r = $this->moreArgs; + foreach ($r as $k => $v) { + if ($v[0] !== '/' && $v[0] !== '~') { + $v = getcwd() . "/$v"; + $v = str_replace('/./', '/', $v); + do { + $v = preg_replace('@/[^/]+/\\.\\./@', '/', $v, 1, $changed); + } while ($changed); + $r[$k] = $v; + } + } + return $r; + } + + /** + * Get a short list of errors with options + * + * @return string + */ + public function getErrorReport() + { + if (empty($this->errors)) { + return ''; + } + $r = "Some arguments did not pass validation:\n"; + foreach ($this->errors as $letter => $arr) { + $r .= " $letter : " . implode(', ', $arr) . "\n"; + } + $r .= "\n"; + return $r; + } + + /** + * @return string + */ + public function getArgumentsListing() + { + $r = "\n"; + foreach ($this->_args as $letter => $arg) { + /* @var Cli\Arg $arg */ + $desc = $arg->getDescription(); + $flag = " -$letter "; + if ($arg->mayHaveValue) { + $flag .= "[VAL]"; + } elseif ($arg->mustHaveValue) { + $flag .= "VAL"; + } + if ($arg->assertFile) { + $flag = str_replace('VAL', 'FILE', $flag); + } elseif ($arg->assertDir) { + $flag = str_replace('VAL', 'DIR', $flag); + } + if ($arg->isRequired()) { + $desc = "(required) $desc"; + } + $flag = str_pad($flag, 12, " ", STR_PAD_RIGHT); + $desc = wordwrap($desc, 70); + $r .= $flag . str_replace("\n", "\n ", $desc) . "\n\n"; + } + return $r; + } + + /** + * Get resource of open input stream. May be STDIN or a file pointer + * to the file specified by an option with 'STDIN'. + * + * @return resource + */ + public function openInput() + { + if (null === $this->_stdin) { + return STDIN; + } else { + $this->_stdin = fopen($this->_stdin, 'rb'); + return $this->_stdin; + } + } + + public function closeInput() + { + if (null !== $this->_stdin) { + fclose($this->_stdin); + } + } + + /** + * Get resource of open output stream. May be STDOUT or a file pointer + * to the file specified by an option with 'STDOUT'. The file will be + * truncated to 0 bytes on opening. + * + * @return resource + */ + public function openOutput() + { + if (null === $this->_stdout) { + return STDOUT; + } else { + $this->_stdout = fopen($this->_stdout, 'wb'); + return $this->_stdout; + } + } + + public function closeOutput() + { + if (null !== $this->_stdout) { + fclose($this->_stdout); + } + } + + /** + * @param string $letter + * @param string $msg + * @param string $value + */ + protected function addError($letter, $msg, $value = null) + { + if ($value !== null) { + $value = var_export($value, 1); + } + $this->errors[$letter][] = sprintf($msg, $value); + } +} + diff --git a/lib/minify/lib/MrClay/Cli/Arg.php b/lib/minify/lib/MrClay/Cli/Arg.php new file mode 100644 index 00000000000..81146a7f10e --- /dev/null +++ b/lib/minify/lib/MrClay/Cli/Arg.php @@ -0,0 +1,181 @@ +values['f.raw'] + * + * Use assertReadable()/assertWritable() to cause the validator to test the file/dir for + * read/write permissions respectively. + * + * @method \MrClay\Cli\Arg mayHaveValue() Assert that the argument, if present, may receive a string value + * @method \MrClay\Cli\Arg mustHaveValue() Assert that the argument, if present, must receive a string value + * @method \MrClay\Cli\Arg assertFile() Assert that the argument's value must specify a file + * @method \MrClay\Cli\Arg assertDir() Assert that the argument's value must specify a directory + * @method \MrClay\Cli\Arg assertReadable() Assert that the specified file/dir must be readable + * @method \MrClay\Cli\Arg assertWritable() Assert that the specified file/dir must be writable + * + * @property-read bool mayHaveValue + * @property-read bool mustHaveValue + * @property-read bool assertFile + * @property-read bool assertDir + * @property-read bool assertReadable + * @property-read bool assertWritable + * @property-read bool useAsInfile + * @property-read bool useAsOutfile + * + * @author Steve Clay + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Arg { + /** + * @return array + */ + public function getDefaultSpec() + { + return array( + 'mayHaveValue' => false, + 'mustHaveValue' => false, + 'assertFile' => false, + 'assertDir' => false, + 'assertReadable' => false, + 'assertWritable' => false, + 'useAsInfile' => false, + 'useAsOutfile' => false, + ); + } + + /** + * @var array + */ + protected $spec = array(); + + /** + * @var bool + */ + protected $required = false; + + /** + * @var string + */ + protected $description = ''; + + /** + * @param bool $isRequired + */ + public function __construct($isRequired = false) + { + $this->spec = $this->getDefaultSpec(); + $this->required = (bool) $isRequired; + if ($isRequired) { + $this->spec['mustHaveValue'] = true; + } + } + + /** + * Assert that the argument's value points to a writable file. When + * Cli::openOutput() is called, a write pointer to this file will + * be provided. + * @return Arg + */ + public function useAsOutfile() + { + $this->spec['useAsOutfile'] = true; + return $this->assertFile()->assertWritable(); + } + + /** + * Assert that the argument's value points to a readable file. When + * Cli::openInput() is called, a read pointer to this file will + * be provided. + * @return Arg + */ + public function useAsInfile() + { + $this->spec['useAsInfile'] = true; + return $this->assertFile()->assertReadable(); + } + + /** + * @return array + */ + public function getSpec() + { + return $this->spec; + } + + /** + * @param string $desc + * @return Arg + */ + public function setDescription($desc) + { + $this->description = $desc; + return $this; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @return bool + */ + public function isRequired() + { + return $this->required; + } + + /** + * Note: magic methods declared in class PHPDOC + * + * @param string $name + * @param array $args + * @return Arg + * @throws \BadMethodCallException + */ + public function __call($name, array $args = array()) + { + if (array_key_exists($name, $this->spec)) { + $this->spec[$name] = true; + if ($name === 'assertFile' || $name === 'assertDir') { + $this->spec['mustHaveValue'] = true; + } + } else { + throw new \BadMethodCallException('Method does not exist'); + } + return $this; + } + + /** + * Note: magic properties declared in class PHPDOC + * + * @param string $name + * @return bool|null + */ + public function __get($name) + { + if (array_key_exists($name, $this->spec)) { + return $this->spec[$name]; + } + return null; + } +} diff --git a/lib/minify/lib/Solar/Dir.php b/lib/minify/lib/Solar/Dir.php deleted file mode 100644 index b4928ef2119..00000000000 --- a/lib/minify/lib/Solar/Dir.php +++ /dev/null @@ -1,199 +0,0 @@ - - * - * @license http://opensource.org/licenses/bsd-license.php BSD - * - * @version $Id$ - * - */ -class Solar_Dir { - - /** - * - * The OS-specific temporary directory location. - * - * @var string - * - */ - protected static $_tmp; - - /** - * - * Hack for [[php::is_dir() | ]] that checks the include_path. - * - * Use this to see if a directory exists anywhere in the include_path. - * - * {{code: php - * $dir = Solar_Dir::exists('path/to/dir') - * if ($dir) { - * $files = scandir($dir); - * } else { - * echo "Not found in the include-path."; - * } - * }} - * - * @param string $dir Check for this directory in the include_path. - * - * @return mixed If the directory exists in the include_path, returns the - * absolute path; if not, returns boolean false. - * - */ - public static function exists($dir) - { - // no file requested? - $dir = trim($dir); - if (! $dir) { - return false; - } - - // using an absolute path for the file? - // dual check for Unix '/' and Windows '\', - // or Windows drive letter and a ':'. - $abs = ($dir[0] == '/' || $dir[0] == '\\' || $dir[1] == ':'); - if ($abs && is_dir($dir)) { - return $dir; - } - - // using a relative path on the file - $path = explode(PATH_SEPARATOR, ini_get('include_path')); - foreach ($path as $base) { - // strip Unix '/' and Windows '\' - $target = rtrim($base, '\\/') . DIRECTORY_SEPARATOR . $dir; - if (is_dir($target)) { - return $target; - } - } - - // never found it - return false; - } - - /** - * - * "Fixes" a directory string for the operating system. - * - * Use slashes anywhere you need a directory separator. Then run the - * string through fixdir() and the slashes will be converted to the - * proper separator (for example '\' on Windows). - * - * Always adds a final trailing separator. - * - * @param string $dir The directory string to 'fix'. - * - * @return string The "fixed" directory string. - * - */ - public static function fix($dir) - { - $dir = str_replace('/', DIRECTORY_SEPARATOR, $dir); - return rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - } - - /** - * - * Convenience method for dirname() and higher-level directories. - * - * @param string $file Get the dirname() of this file. - * - * @param int $up Move up in the directory structure this many - * times, default 0. - * - * @return string The dirname() of the file. - * - */ - public static function name($file, $up = 0) - { - $dir = dirname($file); - while ($up --) { - $dir = dirname($dir); - } - return $dir; - } - - /** - * - * Returns the OS-specific directory for temporary files. - * - * @param string $sub Add this subdirectory to the returned temporary - * directory name. - * - * @return string The temporary directory path. - * - */ - public static function tmp($sub = '') - { - // find the tmp dir if needed - if (! Solar_Dir::$_tmp) { - - // use the system if we can - if (function_exists('sys_get_temp_dir')) { - $tmp = sys_get_temp_dir(); - } else { - $tmp = Solar_Dir::_tmp(); - } - - // remove trailing separator and save - Solar_Dir::$_tmp = rtrim($tmp, DIRECTORY_SEPARATOR); - } - - // do we have a subdirectory request? - $sub = trim($sub); - if ($sub) { - // remove leading and trailing separators, and force exactly - // one trailing separator - $sub = trim($sub, DIRECTORY_SEPARATOR) - . DIRECTORY_SEPARATOR; - } - - return Solar_Dir::$_tmp . DIRECTORY_SEPARATOR . $sub; - } - - /** - * - * Returns the OS-specific temporary directory location. - * - * @return string The temp directory path. - * - */ - 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'; - } -} diff --git a/lib/minify/readme_moodle.txt b/lib/minify/readme_moodle.txt index bc14db52c24..3424587ff28 100644 --- a/lib/minify/readme_moodle.txt +++ b/lib/minify/readme_moodle.txt @@ -1,4 +1,4 @@ -Description of Minify 2.1.3 import into Moodle +Description of Minify 2.1.5 import into Moodle Notes: * Uses are required to add minify/lib to the include path @@ -12,9 +12,6 @@ Usage: Changes: * Removed index.php - Is an unused entry point program and could potentially pose a security risk in the future. + * Removed /builder/* - Not needed * Removed .htaccess - Not needed - * Changed config.php - Changed settings to Moodle specific settings incase this - ever gets accidentally used. - * Updated lib/Minify/CSS/Compressor.php - Applied an upstream fix for MDL-29864 - to allow usage of unquoted font-familes with spaces in CSS. - http://code.google.com/p/minify/issues/detail?id=210 + * Changed config.php - added moodle specific settings diff --git a/lib/minify/utils.php b/lib/minify/utils.php index c7359415207..a18d7ad10b8 100644 --- a/lib/minify/utils.php +++ b/lib/minify/utils.php @@ -1,90 +1,74 @@ - * - * + * + * + * * * - * 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'); + 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'; } - if (! isset($builds[$group])) { - $builds[$group] = new Minify_Build($gc[$group]); + $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 $builds[$group]; + return Minify_HTML_Helper::getLastModified($sources); } diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index f706bdeab89..9c98bbc3193 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -81,7 +81,7 @@ minify Minify BSD - 2.1.3 + 2.1.5 diff --git a/lib/upgradelib.php b/lib/upgradelib.php index 6fed9b10e16..e58c69a9c49 100644 --- a/lib/upgradelib.php +++ b/lib/upgradelib.php @@ -270,6 +270,8 @@ function upgrade_stale_php_files_present() { global $CFG; $someexamplesofremovedfiles = array( + // removed in 2.3dev + '/lib/minify/builder/', // removed in 2.2dev '/lib/yui/3.4.1pr1/', // removed in 2.2