Browse Source
Added $config['minify_js'] to compress Javascript with minify Added $config['additional_javascript_compile'] to put all Javascript files/addons into one file. Moved delete/report post controls to a shared template. Ability to have different Javascript files between boards.pull/40/head
Michael Save
12 years ago
41 changed files with 10018 additions and 29 deletions
File diff suppressed because it is too large
@ -0,0 +1,366 @@ |
|||
<?php |
|||
/** |
|||
* Class HTTP_ConditionalGet |
|||
* @package Minify |
|||
* @subpackage HTTP |
|||
*/ |
|||
|
|||
/** |
|||
* Implement conditional GET via a timestamp or hash of content |
|||
* |
|||
* E.g. Content from DB with update time: |
|||
* <code> |
|||
* list($updateTime, $content) = getDbUpdateAndContent(); |
|||
* $cg = new HTTP_ConditionalGet(array( |
|||
* 'lastModifiedTime' => $updateTime |
|||
* ,'isPublic' => true |
|||
* )); |
|||
* $cg->sendHeaders(); |
|||
* if ($cg->cacheIsValid) { |
|||
* exit(); |
|||
* } |
|||
* echo $content; |
|||
* </code> |
|||
* |
|||
* E.g. Shortcut for the above |
|||
* <code> |
|||
* HTTP_ConditionalGet::check($updateTime, true); // exits if client has cache |
|||
* echo $content; |
|||
* </code> |
|||
* |
|||
* E.g. Content from DB with no update time: |
|||
* <code> |
|||
* $content = getContentFromDB(); |
|||
* $cg = new HTTP_ConditionalGet(array( |
|||
* 'contentHash' => md5($content) |
|||
* )); |
|||
* $cg->sendHeaders(); |
|||
* if ($cg->cacheIsValid) { |
|||
* exit(); |
|||
* } |
|||
* echo $content; |
|||
* </code> |
|||
* |
|||
* E.g. Static content with some static includes: |
|||
* <code> |
|||
* // before content |
|||
* $cg = new HTTP_ConditionalGet(array( |
|||
* 'lastUpdateTime' => max( |
|||
* filemtime(__FILE__) |
|||
* ,filemtime('/path/to/header.inc') |
|||
* ,filemtime('/path/to/footer.inc') |
|||
* ) |
|||
* )); |
|||
* $cg->sendHeaders(); |
|||
* if ($cg->cacheIsValid) { |
|||
* exit(); |
|||
* } |
|||
* </code> |
|||
* @package Minify |
|||
* @subpackage HTTP |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
class HTTP_ConditionalGet { |
|||
|
|||
/** |
|||
* Does the client have a valid copy of the requested resource? |
|||
* |
|||
* You'll want to check this after instantiating the object. If true, do |
|||
* not send content, just call sendHeaders() if you haven't already. |
|||
* |
|||
* @var bool |
|||
*/ |
|||
public $cacheIsValid = null; |
|||
|
|||
/** |
|||
* @param array $spec options |
|||
* |
|||
* '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. |
|||
* |
|||
* 'encoding': (string) if set, the header "Vary: Accept-Encoding" will |
|||
* always be sent and a truncated version of the encoding will be appended |
|||
* to the ETag. E.g. "pub123456;gz". This will also trigger a more lenient |
|||
* checking of the client's If-None-Match header, as the encoding portion of |
|||
* the ETag will be stripped before comparison. |
|||
* |
|||
* 'contentHash': (string) if given, only the ETag header can be sent with |
|||
* content (only HTTP1.1 clients can conditionally GET). The given string |
|||
* should be short with no quote characters and always change when the |
|||
* resource changes (recommend md5()). This is not needed/used if |
|||
* lastModifiedTime is given. |
|||
* |
|||
* 'eTag': (string) if given, this will be used as the ETag header rather |
|||
* than values based on lastModifiedTime or contentHash. Also the encoding |
|||
* string will not be appended to the given value as described above. |
|||
* |
|||
* 'invalidate': (bool) if true, the client cache will be considered invalid |
|||
* without testing. Effectively this disables conditional GET. |
|||
* (default false) |
|||
* |
|||
* 'maxAge': (int) if given, this will set the Cache-Control max-age in |
|||
* 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. |
|||
*/ |
|||
public function __construct($spec) |
|||
{ |
|||
$scope = (isset($spec['isPublic']) && $spec['isPublic']) |
|||
? 'public' |
|||
: 'private'; |
|||
$maxAge = 0; |
|||
// backwards compatibility (can be removed later) |
|||
if (isset($spec['setExpires']) |
|||
&& is_numeric($spec['setExpires']) |
|||
&& ! isset($spec['maxAge'])) { |
|||
$spec['maxAge'] = $spec['setExpires'] - $_SERVER['REQUEST_TIME']; |
|||
} |
|||
if (isset($spec['maxAge'])) { |
|||
$maxAge = $spec['maxAge']; |
|||
$this->_headers['Expires'] = self::gmtDate( |
|||
$_SERVER['REQUEST_TIME'] + $spec['maxAge'] |
|||
); |
|||
} |
|||
$etagAppend = ''; |
|||
if (isset($spec['encoding'])) { |
|||
$this->_stripEtag = true; |
|||
$this->_headers['Vary'] = 'Accept-Encoding'; |
|||
if ('' !== $spec['encoding']) { |
|||
if (0 === strpos($spec['encoding'], 'x-')) { |
|||
$spec['encoding'] = substr($spec['encoding'], 2); |
|||
} |
|||
$etagAppend = ';' . substr($spec['encoding'], 0, 2); |
|||
} |
|||
} |
|||
if (isset($spec['lastModifiedTime'])) { |
|||
$this->_setLastModified($spec['lastModifiedTime']); |
|||
if (isset($spec['eTag'])) { // Use it |
|||
$this->_setEtag($spec['eTag'], $scope); |
|||
} else { // base both headers on time |
|||
$this->_setEtag($spec['lastModifiedTime'] . $etagAppend, $scope); |
|||
} |
|||
} elseif (isset($spec['eTag'])) { // Use it |
|||
$this->_setEtag($spec['eTag'], $scope); |
|||
} elseif (isset($spec['contentHash'])) { // Use the hash as the ETag |
|||
$this->_setEtag($spec['contentHash'] . $etagAppend, $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 |
|||
: $this->_isCacheValid(); |
|||
} |
|||
|
|||
/** |
|||
* Get array of output headers to be sent |
|||
* |
|||
* In the case of 304 responses, this array will only contain the response |
|||
* code header: array('_responseCode' => 'HTTP/1.0 304 Not Modified') |
|||
* |
|||
* Otherwise something like: |
|||
* <code> |
|||
* array( |
|||
* 'Cache-Control' => 'max-age=0, public' |
|||
* ,'ETag' => '"foobar"' |
|||
* ) |
|||
* </code> |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getHeaders() |
|||
{ |
|||
return $this->_headers; |
|||
} |
|||
|
|||
/** |
|||
* Set the Content-Length header in bytes |
|||
* |
|||
* With most PHP configs, as long as you don't flush() output, this method |
|||
* is not needed and PHP will buffer all output and set Content-Length for |
|||
* you. Otherwise you'll want to call this to let the client know up front. |
|||
* |
|||
* @param int $bytes |
|||
* |
|||
* @return int copy of input $bytes |
|||
*/ |
|||
public function setContentLength($bytes) |
|||
{ |
|||
return $this->_headers['Content-Length'] = $bytes; |
|||
} |
|||
|
|||
/** |
|||
* Send headers |
|||
* |
|||
* @see getHeaders() |
|||
* |
|||
* Note this doesn't "clear" the headers. Calling sendHeaders() will |
|||
* call header() again (but probably have not effect) and getHeaders() will |
|||
* still return the headers. |
|||
* |
|||
* @return null |
|||
*/ |
|||
public function sendHeaders() |
|||
{ |
|||
$headers = $this->_headers; |
|||
if (array_key_exists('_responseCode', $headers)) { |
|||
// 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) { |
|||
header($name . ': ' . $val); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Exit if the client's cache is valid for this resource |
|||
* |
|||
* This is a convenience method for common use of the class |
|||
* |
|||
* @param int $lastModifiedTime if given, both ETag AND Last-Modified headers |
|||
* will be sent with content. This is recommended. |
|||
* |
|||
* @param bool $isPublic (default false) if true, the Cache-Control header |
|||
* will contain "public", allowing proxies to cache the content. Otherwise |
|||
* "private" will be sent, allowing only browser caching. |
|||
* |
|||
* @param array $options (default empty) additional options for constructor |
|||
*/ |
|||
public static function check($lastModifiedTime = null, $isPublic = false, $options = array()) |
|||
{ |
|||
if (null !== $lastModifiedTime) { |
|||
$options['lastModifiedTime'] = (int)$lastModifiedTime; |
|||
} |
|||
$options['isPublic'] = (bool)$isPublic; |
|||
$cg = new HTTP_ConditionalGet($options); |
|||
$cg->sendHeaders(); |
|||
if ($cg->cacheIsValid) { |
|||
exit(); |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Get a GMT formatted date for use in HTTP headers |
|||
* |
|||
* <code> |
|||
* header('Expires: ' . HTTP_ConditionalGet::gmtdate($time)); |
|||
* </code> |
|||
* |
|||
* @param int $time unix timestamp |
|||
* |
|||
* @return string |
|||
*/ |
|||
public static function gmtDate($time) |
|||
{ |
|||
return gmdate('D, d M Y H:i:s \G\M\T', $time); |
|||
} |
|||
|
|||
protected $_headers = array(); |
|||
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; |
|||
$this->_headers['Last-Modified'] = self::gmtDate($time); |
|||
} |
|||
|
|||
/** |
|||
* Determine validity of client cache and queue 304 header if valid |
|||
* |
|||
* @return bool |
|||
*/ |
|||
protected function _isCacheValid() |
|||
{ |
|||
if (null === $this->_etag) { |
|||
// lmTime is copied to ETag, so this condition implies that the |
|||
// server sent neither ETag nor Last-Modified, so the client can't |
|||
// possibly has a valid cache. |
|||
return false; |
|||
} |
|||
$isValid = ($this->resourceMatchedEtag() || $this->resourceNotModified()); |
|||
if ($isValid) { |
|||
$this->_headers['_responseCode'] = 'HTTP/1.0 304 Not Modified'; |
|||
} |
|||
return $isValid; |
|||
} |
|||
|
|||
/** |
|||
* @return bool |
|||
*/ |
|||
protected function resourceMatchedEtag() |
|||
{ |
|||
if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) { |
|||
return false; |
|||
} |
|||
$clientEtagList = get_magic_quotes_gpc() |
|||
? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) |
|||
: $_SERVER['HTTP_IF_NONE_MATCH']; |
|||
$clientEtags = explode(',', $clientEtagList); |
|||
|
|||
$compareTo = $this->normalizeEtag($this->_etag); |
|||
foreach ($clientEtags as $clientEtag) { |
|||
if ($this->normalizeEtag($clientEtag) === $compareTo) { |
|||
// respond with the client's matched ETag, even if it's not what |
|||
// we would've sent by default |
|||
$this->_headers['ETag'] = trim($clientEtag); |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* @param string $etag |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function normalizeEtag($etag) { |
|||
$etag = trim($etag); |
|||
return $this->_stripEtag |
|||
? preg_replace('/;\\w\\w"$/', '"', $etag) |
|||
: $etag; |
|||
} |
|||
|
|||
/** |
|||
* @return bool |
|||
*/ |
|||
protected function resourceNotModified() |
|||
{ |
|||
if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { |
|||
return false; |
|||
} |
|||
// 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); |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
} |
@ -0,0 +1,335 @@ |
|||
<?php |
|||
/** |
|||
* Class HTTP_Encoder |
|||
* @package Minify |
|||
* @subpackage HTTP |
|||
*/ |
|||
|
|||
/** |
|||
* Encode and send gzipped/deflated content |
|||
* |
|||
* The "Vary: Accept-Encoding" header is sent. If the client allows encoding, |
|||
* Content-Encoding and Content-Length are added. |
|||
* |
|||
* <code> |
|||
* // Send a CSS file, compressed if possible |
|||
* $he = new HTTP_Encoder(array( |
|||
* 'content' => file_get_contents($cssFile) |
|||
* ,'type' => 'text/css' |
|||
* )); |
|||
* $he->encode(); |
|||
* $he->sendAll(); |
|||
* </code> |
|||
* |
|||
* <code> |
|||
* // Shortcut to encoding output |
|||
* header('Content-Type: text/css'); // needed if not HTML |
|||
* HTTP_Encoder::output($css); |
|||
* </code> |
|||
* |
|||
* <code> |
|||
* // Just sniff for the accepted encoding |
|||
* $encoding = HTTP_Encoder::getAcceptedEncoding(); |
|||
* </code> |
|||
* |
|||
* For more control over headers, use getHeaders() and getData() and send your |
|||
* own output. |
|||
* |
|||
* Note: If you don't need header mgmt, use PHP's native gzencode, gzdeflate, |
|||
* and gzcompress functions for gzip, deflate, and compress-encoding |
|||
* respectively. |
|||
* |
|||
* @package Minify |
|||
* @subpackage HTTP |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
class HTTP_Encoder { |
|||
|
|||
/** |
|||
* Should the encoder allow HTTP encoding to IE6? |
|||
* |
|||
* If you have many IE6 users and the bandwidth savings is worth troubling |
|||
* some of them, set this to true. |
|||
* |
|||
* By default, encoding is only offered to IE7+. When this is true, |
|||
* getAcceptedEncoding() will return an encoding for IE6 if its user agent |
|||
* string contains "SV1". This has been documented in many places as "safe", |
|||
* but there seem to be remaining, intermittent encoding bugs in patched |
|||
* IE6 on the wild web. |
|||
* |
|||
* @var bool |
|||
*/ |
|||
public static $encodeToIe6 = true; |
|||
|
|||
|
|||
/** |
|||
* Default compression level for zlib operations |
|||
* |
|||
* This level is used if encode() is not given a $compressionLevel |
|||
* |
|||
* @var int |
|||
*/ |
|||
public static $compressionLevel = 6; |
|||
|
|||
|
|||
/** |
|||
* Get an HTTP Encoder object |
|||
* |
|||
* @param array $spec options |
|||
* |
|||
* 'content': (string required) content to be encoded |
|||
* |
|||
* 'type': (string) if set, the Content-Type header will have this value. |
|||
* |
|||
* 'method: (string) only set this if you are forcing a particular encoding |
|||
* method. If not set, the best method will be chosen by getAcceptedEncoding() |
|||
* The available methods are 'gzip', 'deflate', 'compress', and '' (no |
|||
* encoding) |
|||
*/ |
|||
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'] = $this->_useMbStrlen |
|||
? (string)mb_strlen($this->_content, '8bit') |
|||
: (string)strlen($this->_content); |
|||
if (isset($spec['type'])) { |
|||
$this->_headers['Content-Type'] = $spec['type']; |
|||
} |
|||
if (isset($spec['method']) |
|||
&& in_array($spec['method'], array('gzip', 'deflate', 'compress', ''))) |
|||
{ |
|||
$this->_encodeMethod = array($spec['method'], $spec['method']); |
|||
} else { |
|||
$this->_encodeMethod = self::getAcceptedEncoding(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get content in current form |
|||
* |
|||
* Call after encode() for encoded content. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getContent() |
|||
{ |
|||
return $this->_content; |
|||
} |
|||
|
|||
/** |
|||
* Get array of output headers to be sent |
|||
* |
|||
* E.g. |
|||
* <code> |
|||
* array( |
|||
* 'Content-Length' => '615' |
|||
* ,'Content-Encoding' => 'x-gzip' |
|||
* ,'Vary' => 'Accept-Encoding' |
|||
* ) |
|||
* </code> |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getHeaders() |
|||
{ |
|||
return $this->_headers; |
|||
} |
|||
|
|||
/** |
|||
* Send output headers |
|||
* |
|||
* 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. |
|||
* |
|||
* @see getHeaders() |
|||
*/ |
|||
public function sendHeaders() |
|||
{ |
|||
foreach ($this->_headers as $name => $val) { |
|||
header($name . ': ' . $val); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Send output headers and content |
|||
* |
|||
* A shortcut for sendHeaders() and echo getContent() |
|||
* |
|||
* 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. |
|||
*/ |
|||
public function sendAll() |
|||
{ |
|||
$this->sendHeaders(); |
|||
echo $this->_content; |
|||
} |
|||
|
|||
/** |
|||
* Determine the client's best encoding method from the HTTP Accept-Encoding |
|||
* header. |
|||
* |
|||
* If no Accept-Encoding header is set, or the browser is IE before v6 SP2, |
|||
* this will return ('', ''), the "identity" encoding. |
|||
* |
|||
* A syntax-aware scan is done of the Accept-Encoding, so the method must |
|||
* be non 0. The methods are favored in order of gzip, deflate, then |
|||
* compress. Deflate is always smallest and generally faster, but is |
|||
* rarely sent by servers, so client support could be buggier. |
|||
* |
|||
* @param bool $allowCompress allow the older compress encoding |
|||
* |
|||
* @param bool $allowDeflate allow the more recent deflate encoding |
|||
* |
|||
* @return array two values, 1st is the actual encoding method, 2nd is the |
|||
* alias of that method to use in the Content-Encoding header (some browsers |
|||
* call gzip "x-gzip" etc.) |
|||
*/ |
|||
public static function getAcceptedEncoding($allowCompress = true, $allowDeflate = true) |
|||
{ |
|||
// @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html |
|||
|
|||
if (! isset($_SERVER['HTTP_ACCEPT_ENCODING']) |
|||
|| self::isBuggyIe()) |
|||
{ |
|||
return array('', ''); |
|||
} |
|||
$ae = $_SERVER['HTTP_ACCEPT_ENCODING']; |
|||
// gzip checks (quick) |
|||
if (0 === strpos($ae, 'gzip,') // most browsers |
|||
|| 0 === strpos($ae, 'deflate, gzip,') // opera |
|||
) { |
|||
return array('gzip', 'gzip'); |
|||
} |
|||
// gzip checks (slow) |
|||
if (preg_match( |
|||
'@(?:^|,)\\s*((?:x-)?gzip)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@' |
|||
,$ae |
|||
,$m)) { |
|||
return array('gzip', $m[1]); |
|||
} |
|||
if ($allowDeflate) { |
|||
// deflate checks |
|||
$aeRev = strrev($ae); |
|||
if (0 === strpos($aeRev, 'etalfed ,') // ie, webkit |
|||
|| 0 === strpos($aeRev, 'etalfed,') // gecko |
|||
|| 0 === strpos($ae, 'deflate,') // opera |
|||
// slow parsing |
|||
|| preg_match( |
|||
'@(?:^|,)\\s*deflate\\s*(?:$|,|;\\s*q=(?:0\\.|1))@', $ae)) { |
|||
return array('deflate', 'deflate'); |
|||
} |
|||
} |
|||
if ($allowCompress && preg_match( |
|||
'@(?:^|,)\\s*((?:x-)?compress)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@' |
|||
,$ae |
|||
,$m)) { |
|||
return array('compress', $m[1]); |
|||
} |
|||
return array('', ''); |
|||
} |
|||
|
|||
/** |
|||
* Encode (compress) the content |
|||
* |
|||
* If the encode method is '' (none) or compression level is 0, or the 'zlib' |
|||
* extension isn't loaded, we return false. |
|||
* |
|||
* Then the appropriate gz_* function is called to compress the content. If |
|||
* this fails, false is returned. |
|||
* |
|||
* The header "Vary: Accept-Encoding" is added. If encoding is successful, |
|||
* the Content-Length header is updated, and Content-Encoding is also added. |
|||
* |
|||
* @param int $compressionLevel given to zlib functions. If not given, the |
|||
* class default will be used. |
|||
* |
|||
* @return bool success true if the content was actually compressed |
|||
*/ |
|||
public function encode($compressionLevel = null) |
|||
{ |
|||
if (! self::isBuggyIe()) { |
|||
$this->_headers['Vary'] = 'Accept-Encoding'; |
|||
} |
|||
if (null === $compressionLevel) { |
|||
$compressionLevel = self::$compressionLevel; |
|||
} |
|||
if ('' === $this->_encodeMethod[0] |
|||
|| ($compressionLevel == 0) |
|||
|| !extension_loaded('zlib')) |
|||
{ |
|||
return false; |
|||
} |
|||
if ($this->_encodeMethod[0] === 'deflate') { |
|||
$encoded = gzdeflate($this->_content, $compressionLevel); |
|||
} elseif ($this->_encodeMethod[0] === 'gzip') { |
|||
$encoded = gzencode($this->_content, $compressionLevel); |
|||
} else { |
|||
$encoded = gzcompress($this->_content, $compressionLevel); |
|||
} |
|||
if (false === $encoded) { |
|||
return false; |
|||
} |
|||
$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; |
|||
} |
|||
|
|||
/** |
|||
* Encode and send appropriate headers and content |
|||
* |
|||
* This is a convenience method for common use of the class |
|||
* |
|||
* @param string $content |
|||
* |
|||
* @param int $compressionLevel given to zlib functions. If not given, the |
|||
* class default will be used. |
|||
* |
|||
* @return bool success true if the content was actually compressed |
|||
*/ |
|||
public static function output($content, $compressionLevel = null) |
|||
{ |
|||
if (null === $compressionLevel) { |
|||
$compressionLevel = self::$compressionLevel; |
|||
} |
|||
$he = new HTTP_Encoder(array('content' => $content)); |
|||
$ret = $he->encode($compressionLevel); |
|||
$he->sendAll(); |
|||
return $ret; |
|||
} |
|||
|
|||
/** |
|||
* Is the browser an IE version earlier than 6 SP2? |
|||
* |
|||
* @return bool |
|||
*/ |
|||
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 ') |
|||
|| false !== strpos($ua, 'Opera')) { |
|||
return false; |
|||
} |
|||
// no regex = faaast |
|||
$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; |
|||
} |
@ -0,0 +1,385 @@ |
|||
<?php |
|||
/** |
|||
* JSMin.php - modified PHP implementation of Douglas Crockford's JSMin. |
|||
* |
|||
* <code> |
|||
* $minifiedJs = JSMin::minify($js); |
|||
* </code> |
|||
* |
|||
* This is a modified port of jsmin.c. Improvements: |
|||
* |
|||
* 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 |
|||
* same terms as jsmin.c, which has the following license: |
|||
* |
|||
* -- |
|||
* Copyright (c) 2002 Douglas Crockford (www.crockford.com) |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining a copy of |
|||
* this software and associated documentation files (the "Software"), to deal in |
|||
* the Software without restriction, including without limitation the rights to |
|||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
|||
* of the Software, and to permit persons to whom the Software is furnished to do |
|||
* so, subject to the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be included in all |
|||
* copies or substantial portions of the Software. |
|||
* |
|||
* The Software shall be used for Good, not Evil. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
* SOFTWARE. |
|||
* -- |
|||
* |
|||
* @package JSMin |
|||
* @author Ryan Grove <ryan@wonko.com> (PHP port) |
|||
* @author Steve Clay <steve@mrclay.org> (modifications + cleanup) |
|||
* @author Andrea Giammarchi <http://www.3site.eu> (spaceBeforeRegExp) |
|||
* @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c) |
|||
* @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port) |
|||
* @license http://opensource.org/licenses/mit-license.php MIT License |
|||
* @link http://code.google.com/p/jsmin-php/ |
|||
*/ |
|||
|
|||
class JSMin { |
|||
const ORD_LF = 10; |
|||
const ORD_SPACE = 32; |
|||
const ACTION_KEEP_A = 1; |
|||
const ACTION_DELETE_A = 2; |
|||
const ACTION_DELETE_A_B = 3; |
|||
|
|||
protected $a = "\n"; |
|||
protected $b = ''; |
|||
protected $input = ''; |
|||
protected $inputIndex = 0; |
|||
protected $inputLength = 0; |
|||
protected $lookAhead = null; |
|||
protected $output = ''; |
|||
protected $lastByteOut = ''; |
|||
|
|||
/** |
|||
* Minify Javascript. |
|||
* |
|||
* @param string $js Javascript to be minified |
|||
* |
|||
* @return string |
|||
*/ |
|||
public static function minify($js) |
|||
{ |
|||
$jsmin = new JSMin($js); |
|||
return $jsmin->min(); |
|||
} |
|||
|
|||
/** |
|||
* @param string $input |
|||
*/ |
|||
public function __construct($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->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; |
|||
// in case of mbstring.func_overload & 2, must check for null b, |
|||
// otherwise mb_strpos will give WARNING |
|||
} elseif ($this->b === null |
|||
|| (false === strpos('{[(+-', $this->b) |
|||
&& ! $this->isAlphaNum($this->b))) { |
|||
$command = self::ACTION_DELETE_A; |
|||
} |
|||
} elseif (! $this->isAlphaNum($this->a)) { |
|||
if ($this->b === ' ' |
|||
|| ($this->b === "\n" |
|||
&& (false === strpos('}])+-"\'', $this->a)))) { |
|||
$command = self::ACTION_DELETE_A_B; |
|||
} |
|||
} |
|||
$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; |
|||
if ($this->a === "'" || $this->a === '"') { // string literal |
|||
$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( |
|||
"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; |
|||
} |
|||
} |
|||
} |
|||
// fallthrough |
|||
case self::ACTION_DELETE_A_B: |
|||
$this->b = $this->next(); |
|||
if ($this->b === '/' && $this->isRegexpLiteral()) { // RegExp literal |
|||
$this->output .= $this->a . $this->b; |
|||
$pattern = '/'; // in case needed for exception |
|||
while (true) { |
|||
$this->a = $this->get(); |
|||
$pattern .= $this->a; |
|||
if ($this->a === '/') { // end pattern |
|||
break; // while (true) |
|||
} elseif ($this->a === '\\') { |
|||
$this->output .= $this->a; |
|||
$this->a = $this->get(); |
|||
$pattern .= $this->a; |
|||
} elseif (ord($this->a) <= self::ORD_LF) { |
|||
throw new JSMin_UnterminatedRegExpException( |
|||
"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 |
|||
return true; |
|||
} |
|||
if (' ' === $this->a) { |
|||
$length = strlen($this->output); |
|||
if ($length < 2) { // weird edge case |
|||
return true; |
|||
} |
|||
// you can't divide a keyword |
|||
if (preg_match('/(?:case|else|in|return|typeof)$/', $this->output, $m)) { |
|||
if ($this->output === $m[0]) { // odd but could happen |
|||
return true; |
|||
} |
|||
// make sure it's a keyword, not end of an identifier |
|||
$charBeforeKeyword = substr($this->output, $length - strlen($m[0]) - 1, 1); |
|||
if (! $this->isAlphaNum($charBeforeKeyword)) { |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Get next char. Convert ctrl char to space. |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function get() |
|||
{ |
|||
$c = $this->lookAhead; |
|||
$this->lookAhead = null; |
|||
if ($c === null) { |
|||
if ($this->inputIndex < $this->inputLength) { |
|||
$c = $this->input[$this->inputIndex]; |
|||
$this->inputIndex += 1; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
if ($c === "\r" || $c === "\n") { |
|||
return "\n"; |
|||
} |
|||
if (ord($c) < self::ORD_SPACE) { // control char |
|||
return ' '; |
|||
} |
|||
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 = ''; |
|||
while (true) { |
|||
$get = $this->get(); |
|||
$comment .= $get; |
|||
if (ord($get) <= self::ORD_LF) { // EOL reached |
|||
// if IE conditional comment |
|||
if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) { |
|||
return "/{$comment}"; |
|||
} |
|||
return $get; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
* @throws JSMin_UnterminatedCommentException |
|||
*/ |
|||
protected function multipleLineComment() |
|||
{ |
|||
$this->get(); |
|||
$comment = ''; |
|||
while (true) { |
|||
$get = $this->get(); |
|||
if ($get === '*') { |
|||
if ($this->peek() === '/') { // end of comment reached |
|||
$this->get(); |
|||
// if comment preserved by YUI Compressor |
|||
if (0 === strpos($comment, '!')) { |
|||
return "\n/*!" . substr($comment, 1) . "*/\n"; |
|||
} |
|||
// if IE conditional comment |
|||
if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) { |
|||
return "/*{$comment}*/"; |
|||
} |
|||
return ' '; |
|||
} |
|||
} elseif ($get === null) { |
|||
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() |
|||
{ |
|||
$get = $this->get(); |
|||
if ($get !== '/') { |
|||
return $get; |
|||
} |
|||
switch ($this->peek()) { |
|||
case '/': return $this->singleLineComment(); |
|||
case '*': return $this->multipleLineComment(); |
|||
default: return $get; |
|||
} |
|||
} |
|||
} |
|||
|
|||
class JSMin_UnterminatedStringException extends Exception {} |
|||
class JSMin_UnterminatedCommentException extends Exception {} |
|||
class JSMin_UnterminatedRegExpException extends Exception {} |
File diff suppressed because it is too large
@ -0,0 +1,617 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Minify_Source |
|||
*/ |
|||
require_once 'Minify/Source.php'; |
|||
|
|||
/** |
|||
* Minify - Combines, minifies, and caches JavaScript and CSS files on demand. |
|||
* |
|||
* See README for usage instructions (for now). |
|||
* |
|||
* This library was inspired by {@link mailto:[email protected] jscsscomp by Maxim Martynyuk} |
|||
* and by the article {@link http://www.hunlock.com/blogs/Supercharged_Javascript "Supercharged JavaScript" by Patrick Hunlock}. |
|||
* |
|||
* Requires PHP 5.1.0. |
|||
* Tested on PHP 5.1.6. |
|||
* |
|||
* @package Minify |
|||
* @author Ryan Grove <ryan@wonko.com> |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
* @copyright 2008 Ryan Grove, Stephen Clay. All rights reserved. |
|||
* @license http://opensource.org/licenses/bsd-license.php New BSD License |
|||
* @link http://code.google.com/p/minify/ |
|||
*/ |
|||
class Minify { |
|||
|
|||
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? |
|||
* |
|||
* If you upload files from Windows to a non-Windows server, Windows may report |
|||
* incorrect mtimes for the files. Immediately after modifying and uploading a |
|||
* file, use the touch command to update the mtime on the server. If the mtime |
|||
* jumps ahead by a number of hours, set this variable to that number. If the mtime |
|||
* moves back, this should not be needed. |
|||
* |
|||
* @var int $uploaderHoursBehind |
|||
*/ |
|||
public static $uploaderHoursBehind = 0; |
|||
|
|||
/** |
|||
* If this string is not empty AND the serve() option 'bubbleCssImports' is |
|||
* NOT set, then serve() will check CSS files for @import declarations that |
|||
* appear too late in the combined stylesheet. If found, serve() will prepend |
|||
* the output with this warning. |
|||
* |
|||
* @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. |
|||
* |
|||
* If not called, Minify will not use a cache and, for each 200 response, will |
|||
* need to recombine files, minify and encode the output. |
|||
* |
|||
* @param mixed $cache object with identical interface as Minify_Cache_File or |
|||
* a directory path, or null to disable caching. (default = '') |
|||
* |
|||
* @param bool $fileLocking (default = true) This only applies if the first |
|||
* parameter is a string. |
|||
* |
|||
* @return null |
|||
*/ |
|||
public static function setCache($cache = '', $fileLocking = true) |
|||
{ |
|||
if (is_string($cache)) { |
|||
require_once 'Minify/Cache/File.php'; |
|||
self::$_cache = new Minify_Cache_File($cache, $fileLocking); |
|||
} else { |
|||
self::$_cache = $cache; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Serve a request for a minified file. |
|||
* |
|||
* Here are the available options and defaults in the base controller: |
|||
* |
|||
* 'isPublic' : send "public" instead of "private" in Cache-Control |
|||
* headers, allowing shared caches to cache the output. (default true) |
|||
* |
|||
* 'quiet' : set to true to have serve() return an array rather than sending |
|||
* any headers/output (default false) |
|||
* |
|||
* 'encodeOutput' : set to false to disable content encoding, and not send |
|||
* the Vary header (default true) |
|||
* |
|||
* 'encodeMethod' : generally you should let this be determined by |
|||
* HTTP_Encoder (leave null), but you can force a particular encoding |
|||
* to be returned, by setting this to 'gzip' or '' (no encoding) |
|||
* |
|||
* 'encodeLevel' : level of encoding compression (0 to 9, default 9) |
|||
* |
|||
* 'contentTypeCharset' : appended to the Content-Type header sent. Set to a falsey |
|||
* value to remove. (default 'utf-8') |
|||
* |
|||
* 'maxAge' : set this to the number of seconds the client should use its cache |
|||
* before revalidating with the server. This sets Cache-Control: max-age and the |
|||
* Expires header. Unlike the old 'setExpires' setting, this setting will NOT |
|||
* prevent conditional GETs. Note this has nothing to do with server-side caching. |
|||
* |
|||
* 'rewriteCssUris' : If true, serve() will automatically set the 'currentDir' |
|||
* minifier option to enable URI rewriting in CSS files (default true) |
|||
* |
|||
* 'bubbleCssImports' : If true, all @import declarations in combined CSS |
|||
* files will be move to the top. Note this may alter effective CSS values |
|||
* due to a change in order. (default false) |
|||
* |
|||
* 'debug' : set to true to minify all sources with the 'Lines' controller, which |
|||
* eases the debugging of combined files. This also prevents 304 responses. |
|||
* @see Minify_Lines::minify() |
|||
* |
|||
* 'minifiers' : to override Minify's default choice of minifier function for |
|||
* a particular content-type, specify your callback under the key of the |
|||
* content-type: |
|||
* <code> |
|||
* // call customCssMinifier($css) for all CSS minification |
|||
* $options['minifiers'][Minify::TYPE_CSS] = 'customCssMinifier'; |
|||
* |
|||
* // don't minify Javascript at all |
|||
* $options['minifiers'][Minify::TYPE_JS] = ''; |
|||
* </code> |
|||
* |
|||
* 'minifierOptions' : to send options to the minifier function, specify your options |
|||
* under the key of the content-type. E.g. To send the CSS minifier an option: |
|||
* <code> |
|||
* // give CSS minifier array('optionName' => 'optionValue') as 2nd argument |
|||
* $options['minifierOptions'][Minify::TYPE_CSS]['optionName'] = 'optionValue'; |
|||
* </code> |
|||
* |
|||
* 'contentType' : (optional) this is only needed if your file extension is not |
|||
* js/css/html. The given content-type will be sent regardless of source file |
|||
* extension, so this should not be used in a Groups config with other |
|||
* Javascript/CSS files. |
|||
* |
|||
* Any controller options are documented in that controller's setupSources() method. |
|||
* |
|||
* @param mixed $controller instance of subclass of Minify_Controller_Base or string |
|||
* name of controller. E.g. 'Files' |
|||
* |
|||
* @param array $options controller/serve options |
|||
* |
|||
* @return mixed null, or, if the 'quiet' option is set to true, an array |
|||
* with keys "success" (bool), "statusCode" (int), "content" (string), and |
|||
* "headers" (array). |
|||
*/ |
|||
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; |
|||
if (! class_exists($class, false)) { |
|||
require_once "Minify/Controller/" |
|||
. str_replace('_', '/', $controller) . ".php"; |
|||
} |
|||
$controller = new $class(); |
|||
/* @var Minify_Controller_Base $controller */ |
|||
} |
|||
|
|||
// set up controller sources and mix remaining options with |
|||
// controller defaults |
|||
$options = $controller->setupSources($options); |
|||
$options = $controller->analyzeSources($options); |
|||
self::$_options = $controller->mixInDefaultOptions($options); |
|||
|
|||
// check request validity |
|||
if (! $controller->sources) { |
|||
// invalid request! |
|||
if (! self::$_options['quiet']) { |
|||
self::_errorExit(self::$_options['badRequestHeader'], self::URL_DEBUG); |
|||
} else { |
|||
list(,$statusCode) = explode(' ', self::$_options['badRequestHeader']); |
|||
return array( |
|||
'success' => false |
|||
,'statusCode' => (int)$statusCode |
|||
,'content' => '' |
|||
,'headers' => array() |
|||
); |
|||
} |
|||
} |
|||
|
|||
self::$_controller = $controller; |
|||
|
|||
if (self::$_options['debug']) { |
|||
self::_setupDebug($controller->sources); |
|||
self::$_options['maxAge'] = 0; |
|||
} |
|||
|
|||
// determine encoding |
|||
if (self::$_options['encodeOutput']) { |
|||
$sendVary = true; |
|||
if (self::$_options['encodeMethod'] !== null) { |
|||
// controller specifically requested this |
|||
$contentEncoding = self::$_options['encodeMethod']; |
|||
} else { |
|||
// sniff request header |
|||
require_once 'HTTP/Encoder.php'; |
|||
// depending on what the client accepts, $contentEncoding may be |
|||
// '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) |
|||
} |
|||
|
|||
// check client cache |
|||
require_once 'HTTP/ConditionalGet.php'; |
|||
$cgOptions = array( |
|||
'lastModifiedTime' => self::$_options['lastModifiedTime'] |
|||
,'isPublic' => self::$_options['isPublic'] |
|||
,'encoding' => self::$_options['encodeMethod'] |
|||
); |
|||
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) { |
|||
// client's cache is valid |
|||
if (! self::$_options['quiet']) { |
|||
$cg->sendHeaders(); |
|||
return; |
|||
} else { |
|||
return array( |
|||
'success' => true |
|||
,'statusCode' => 304 |
|||
,'content' => '' |
|||
,'headers' => $cg->getHeaders() |
|||
); |
|||
} |
|||
} else { |
|||
// client will need output |
|||
$headers = $cg->getHeaders(); |
|||
unset($cg); |
|||
} |
|||
|
|||
if (self::$_options['contentType'] === self::TYPE_CSS |
|||
&& self::$_options['rewriteCssUris']) { |
|||
foreach($controller->sources as $key => $source) { |
|||
if ($source->filepath |
|||
&& !isset($source->minifyOptions['currentDir']) |
|||
&& !isset($source->minifyOptions['prependRelativePath']) |
|||
) { |
|||
$source->minifyOptions['currentDir'] = dirname($source->filepath); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// check server 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 = self::_getCacheId(); |
|||
$fullCacheId = (self::$_options['encodeMethod']) |
|||
? $cacheId . '.gz' |
|||
: $cacheId; |
|||
// check cache for valid entry |
|||
$cacheIsReady = self::$_cache->isValid($fullCacheId, self::$_options['lastModifiedTime']); |
|||
if ($cacheIsReady) { |
|||
$cacheContentLength = self::$_cache->getSize($fullCacheId); |
|||
} else { |
|||
// generate & cache content |
|||
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'])); |
|||
} |
|||
} |
|||
} else { |
|||
// no cache |
|||
$cacheIsReady = false; |
|||
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 |
|||
$content = gzencode($content, self::$_options['encodeLevel']); |
|||
} |
|||
|
|||
// add headers |
|||
$headers['Content-Length'] = $cacheIsReady |
|||
? $cacheContentLength |
|||
: ((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'] && $sendVary) { |
|||
$headers['Vary'] = 'Accept-Encoding'; |
|||
} |
|||
|
|||
if (! self::$_options['quiet']) { |
|||
// output headers & content |
|||
foreach ($headers as $name => $val) { |
|||
header($name . ': ' . $val); |
|||
} |
|||
if ($cacheIsReady) { |
|||
self::$_cache->display($fullCacheId); |
|||
} else { |
|||
echo $content; |
|||
} |
|||
} else { |
|||
return array( |
|||
'success' => true |
|||
,'statusCode' => 200 |
|||
,'content' => $cacheIsReady |
|||
? self::$_cache->fetch($fullCacheId) |
|||
: $content |
|||
,'headers' => $headers |
|||
); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Return combined minified content for a set of sources |
|||
* |
|||
* No internal caching will be used and the content will not be HTTP encoded. |
|||
* |
|||
* @param array $sources array of filepaths and/or Minify_Source objects |
|||
* |
|||
* @param array $options (optional) array of options for serve. By default |
|||
* these are already set: quiet = true, encodeMethod = '', lastModifiedTime = 0. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public static function combine($sources, $options = array()) |
|||
{ |
|||
$cache = self::$_cache; |
|||
self::$_cache = null; |
|||
$options = array_merge(array( |
|||
'files' => (array)$sources |
|||
,'quiet' => true |
|||
,'encodeMethod' => '' |
|||
,'lastModifiedTime' => 0 |
|||
), $options); |
|||
$out = self::serve('Files', $options); |
|||
self::$_cache = $cache; |
|||
return $out['content']; |
|||
} |
|||
|
|||
/** |
|||
* Set $_SERVER['DOCUMENT_ROOT']. On IIS, the value is created from SCRIPT_FILENAME and SCRIPT_NAME. |
|||
* |
|||
* @param string $docRoot value to use for DOCUMENT_ROOT |
|||
*/ |
|||
public static function setDocRoot($docRoot = '') |
|||
{ |
|||
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['SCRIPT_FILENAME']) - strlen($_SERVER['SCRIPT_NAME'])); |
|||
$_SERVER['DOCUMENT_ROOT'] = rtrim($_SERVER['DOCUMENT_ROOT'], '\\'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Any Minify_Cache_* object or null (i.e. no server cache is used) |
|||
* |
|||
* @var Minify_Cache_File |
|||
*/ |
|||
private static $_cache = null; |
|||
|
|||
/** |
|||
* Active controller for current request |
|||
* |
|||
* @var Minify_Controller_Base |
|||
*/ |
|||
protected static $_controller = null; |
|||
|
|||
/** |
|||
* 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>$h1</h1>"; |
|||
echo "<p>Please see <a href='$url'>$url</a>.</p>"; |
|||
exit(); |
|||
} |
|||
|
|||
/** |
|||
* Set up sources to use Minify_Lines |
|||
* |
|||
* @param array $sources Minify_Source instances |
|||
*/ |
|||
protected static function _setupDebug($sources) |
|||
{ |
|||
foreach ($sources as $source) { |
|||
$source->minifier = array('Minify_Lines', 'minify'); |
|||
$id = $source->getId(); |
|||
$source->minifyOptions = array( |
|||
'id' => (is_file($id) ? basename($id) : $id) |
|||
); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Combines sources and minifies the result. |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected static function _combineMinify() |
|||
{ |
|||
$type = self::$_options['contentType']; // ease readability |
|||
|
|||
// when combining scripts, make sure all statements separated and |
|||
// trailing single line comment is terminated |
|||
$implodeSeparator = ($type === self::TYPE_JS) |
|||
? "\n;" |
|||
: ''; |
|||
// allow the user to pass a particular array of options to each |
|||
// minifier (designated by type). source objects may still override |
|||
// these |
|||
$defaultOptions = isset(self::$_options['minifierOptions'][$type]) |
|||
? self::$_options['minifierOptions'][$type] |
|||
: array(); |
|||
// if minifier not set, default is no minification. source objects |
|||
// may still override this |
|||
$defaultMinifier = isset(self::$_options['minifiers'][$type]) |
|||
? self::$_options['minifiers'][$type] |
|||
: false; |
|||
|
|||
// 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 |
|||
: $defaultMinifier; |
|||
$options = (null !== $source->minifyOptions) |
|||
? array_merge($defaultOptions, $source->minifyOptions) |
|||
: $defaultOptions; |
|||
} |
|||
// 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 { |
|||
$content[] = $imploded; |
|||
} |
|||
} |
|||
// 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); |
|||
} |
|||
|
|||
// do any post-processing (esp. for editing build URIs) |
|||
if (self::$_options['postprocessorRequire']) { |
|||
require_once self::$_options['postprocessorRequire']; |
|||
} |
|||
if (self::$_options['postprocessor']) { |
|||
$content = call_user_func(self::$_options['postprocessor'], $content, $type); |
|||
} |
|||
return $content; |
|||
} |
|||
|
|||
/** |
|||
* Make a unique cache id for for this request. |
|||
* |
|||
* Any settings that could affect output are taken into consideration |
|||
* |
|||
* @param string $prefix |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected static function _getCacheId($prefix = 'minify') |
|||
{ |
|||
$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. |
|||
* |
|||
* @param string $css |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected static function _handleCssImports($css) |
|||
{ |
|||
if (self::$_options['bubbleCssImports']) { |
|||
// bubble CSS imports |
|||
preg_match_all('/@import.*?;/', $css, $imports); |
|||
$css = implode('', $imports[0]) . preg_replace('/@import.*?;/', '', $css); |
|||
} else if ('' !== self::$importWarning) { |
|||
// remove comments so we don't mistake { in a comment as a block |
|||
$noCommentCss = preg_replace('@/\\*[\\s\\S]*?\\*/@', '', $css); |
|||
$lastImportPos = strrpos($noCommentCss, '@import'); |
|||
$firstBlockPos = strpos($noCommentCss, '{'); |
|||
if (false !== $lastImportPos |
|||
&& false !== $firstBlockPos |
|||
&& $firstBlockPos < $lastImportPos |
|||
) { |
|||
// { appears before @import : prepend warning |
|||
$css = self::$importWarning . $css; |
|||
} |
|||
} |
|||
return $css; |
|||
} |
|||
} |
@ -0,0 +1,103 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Build |
|||
* @package Minify |
|||
*/ |
|||
|
|||
require_once 'Minify/Source.php'; |
|||
|
|||
/** |
|||
* Maintain a single last modification time for a group of Minify sources to |
|||
* allow use of far off Expires headers in Minify. |
|||
* |
|||
* <code> |
|||
* // in config file |
|||
* $groupSources = array( |
|||
* 'js' => array('file1.js', 'file2.js') |
|||
* ,'css' => array('file1.css', 'file2.css', 'file3.css') |
|||
* ) |
|||
* |
|||
* // during HTML generation |
|||
* $jsBuild = new Minify_Build($groupSources['js']); |
|||
* $cssBuild = new Minify_Build($groupSources['css']); |
|||
* |
|||
* $script = "<script type='text/javascript' src='" |
|||
* . $jsBuild->uri('/min.php/js') . "'></script>"; |
|||
* $link = "<link rel='stylesheet' type='text/css' href='" |
|||
* . $cssBuild->uri('/min.php/css') . "'>"; |
|||
* |
|||
* // in min.php |
|||
* Minify::serve('Groups', array( |
|||
* 'groups' => $groupSources |
|||
* ,'setExpires' => (time() + 86400 * 365) |
|||
* )); |
|||
* </code> |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
class Minify_Build { |
|||
|
|||
/** |
|||
* Last modification time of all files in the build |
|||
* |
|||
* @var int |
|||
*/ |
|||
public $lastModified = 0; |
|||
|
|||
/** |
|||
* String to use as ampersand in uri(). Set this to '&' if |
|||
* you are not HTML-escaping URIs. |
|||
* |
|||
* @var string |
|||
*/ |
|||
public static $ampersand = '&'; |
|||
|
|||
/** |
|||
* Get a time-stamped URI |
|||
* |
|||
* <code> |
|||
* echo $b->uri('/site.js'); |
|||
* // outputs "/site.js?1678242" |
|||
* |
|||
* echo $b->uri('/scriptaculous.js?load=effects'); |
|||
* // outputs "/scriptaculous.js?load=effects&1678242" |
|||
* </code> |
|||
* |
|||
* @param string $uri |
|||
* @param boolean $forceAmpersand (default = false) Force the use of ampersand to |
|||
* append the timestamp to the URI. |
|||
* @return string |
|||
*/ |
|||
public function uri($uri, $forceAmpersand = false) { |
|||
$sep = ($forceAmpersand || strpos($uri, '?') !== false) |
|||
? self::$ampersand |
|||
: '?'; |
|||
return "{$uri}{$sep}{$this->lastModified}"; |
|||
} |
|||
|
|||
/** |
|||
* Create a build object |
|||
* |
|||
* @param array $sources array of Minify_Source objects and/or file paths |
|||
* |
|||
* @return null |
|||
*/ |
|||
public function __construct($sources) |
|||
{ |
|||
$max = 0; |
|||
foreach ((array)$sources as $source) { |
|||
if ($source instanceof Minify_Source) { |
|||
$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)); |
|||
} |
|||
} |
|||
} |
|||
$this->lastModified = $max; |
|||
} |
|||
} |
@ -0,0 +1,99 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_CSS |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Minify CSS |
|||
* |
|||
* This class uses Minify_CSS_Compressor and Minify_CSS_UriRewriter to |
|||
* minify CSS and rewrite relative URIs. |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
* @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.: |
|||
* <code> |
|||
* array('//symlink' => '/real/target/path') // unix |
|||
* array('//static' => 'D:\\staticStorage') // Windows |
|||
* </code> |
|||
* |
|||
* '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'] |
|||
); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,249 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_CSS_Compressor |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Compress CSS |
|||
* |
|||
* This is a heavy regex-based removal of whitespace, unnecessary |
|||
* comments and tokens, and some CSS value minimization, where practical. |
|||
* Many steps have been taken to avoid breaking comment-based hacks, |
|||
* including the ie5/mac filter (and its inversion), but expect tricky |
|||
* hacks involving comment tokens in 'content' value strings to break |
|||
* minimization badly. A test suite is available. |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
* @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]; |
|||
} |
|||
} |
@ -0,0 +1,310 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_CSS_UriRewriter |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Rewrite file-relative URIs as root-relative in CSS files |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
class Minify_CSS_UriRewriter { |
|||
|
|||
/** |
|||
* rewrite() and rewriteRelative() append debugging information here |
|||
* |
|||
* @var string |
|||
*/ |
|||
public static $debugText = ''; |
|||
|
|||
/** |
|||
* In CSS content, rewrite file relative URIs as root relative |
|||
* |
|||
* @param string $css |
|||
* |
|||
* @param string $currentDir The directory of the current CSS file. |
|||
* |
|||
* @param string $docRoot The document root of the web site in which |
|||
* the CSS file resides (default = $_SERVER['DOCUMENT_ROOT']). |
|||
* |
|||
* @param array $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.: |
|||
* <code> |
|||
* array('//symlink' => '/real/target/path') // unix |
|||
* array('//static' => 'D:\\staticStorage') // Windows |
|||
* </code> |
|||
* |
|||
* @return string |
|||
*/ |
|||
public static function rewrite($css, $currentDir, $docRoot = null, $symlinks = array()) |
|||
{ |
|||
self::$_docRoot = self::_realpath( |
|||
$docRoot ? $docRoot : $_SERVER['DOCUMENT_ROOT'] |
|||
); |
|||
self::$_currentDir = self::_realpath($currentDir); |
|||
self::$_symlinks = array(); |
|||
|
|||
// normalize symlinks |
|||
foreach ($symlinks as $link => $target) { |
|||
$link = ($link === '//') |
|||
? self::$_docRoot |
|||
: str_replace('//', self::$_docRoot . '/', $link); |
|||
$link = strtr($link, '/', DIRECTORY_SEPARATOR); |
|||
self::$_symlinks[$link] = self::_realpath($target); |
|||
} |
|||
|
|||
self::$debugText .= "docRoot : " . self::$_docRoot . "\n" |
|||
. "currentDir : " . self::$_currentDir . "\n"; |
|||
if (self::$_symlinks) { |
|||
self::$debugText .= "symlinks : " . var_export(self::$_symlinks, 1) . "\n"; |
|||
} |
|||
self::$debugText .= "\n"; |
|||
|
|||
$css = self::_trimUrls($css); |
|||
|
|||
// rewrite |
|||
$css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/' |
|||
,array(self::$className, '_processUriCB'), $css); |
|||
$css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/' |
|||
,array(self::$className, '_processUriCB'), $css); |
|||
|
|||
return $css; |
|||
} |
|||
|
|||
/** |
|||
* In CSS content, prepend a path to relative URIs |
|||
* |
|||
* @param string $css |
|||
* |
|||
* @param string $path The path to prepend. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public static function prepend($css, $path) |
|||
{ |
|||
self::$_prependPath = $path; |
|||
|
|||
$css = self::_trimUrls($css); |
|||
|
|||
// append |
|||
$css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/' |
|||
,array(self::$className, '_processUriCB'), $css); |
|||
$css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/' |
|||
,array(self::$className, '_processUriCB'), $css); |
|||
|
|||
self::$_prependPath = null; |
|||
return $css; |
|||
} |
|||
|
|||
/** |
|||
* Get a root relative URI from a file relative URI |
|||
* |
|||
* <code> |
|||
* Minify_CSS_UriRewriter::rewriteRelative( |
|||
* '../img/hello.gif' |
|||
* , '/home/user/www/css' // path of CSS file |
|||
* , '/home/user/www' // doc root |
|||
* ); |
|||
* // returns '/img/hello.gif' |
|||
* |
|||
* // example where static files are stored in a symlinked directory |
|||
* Minify_CSS_UriRewriter::rewriteRelative( |
|||
* 'hello.gif' |
|||
* , '/var/staticFiles/theme' |
|||
* , '/home/user/www' |
|||
* , array('/home/user/www/static' => '/var/staticFiles') |
|||
* ); |
|||
* // returns '/static/theme/hello.gif' |
|||
* </code> |
|||
* |
|||
* @param string $uri file relative URI |
|||
* |
|||
* @param string $realCurrentDir realpath of the current file's directory. |
|||
* |
|||
* @param string $realDocRoot realpath of the site document root. |
|||
* |
|||
* @param array $symlinks (default = array()) If the file is stored in |
|||
* a symlink-ed directory, provide an array of link paths to |
|||
* real target paths, where the link paths "appear" to be within the document |
|||
* root. E.g.: |
|||
* <code> |
|||
* array('/home/foo/www/not/real/path' => '/real/target/path') // unix |
|||
* array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path') // Windows |
|||
* </code> |
|||
* |
|||
* @return string |
|||
*/ |
|||
public static function rewriteRelative($uri, $realCurrentDir, $realDocRoot, $symlinks = array()) |
|||
{ |
|||
// prepend path with current dir separator (OS-independent) |
|||
$path = strtr($realCurrentDir, '/', DIRECTORY_SEPARATOR) |
|||
. DIRECTORY_SEPARATOR . strtr($uri, '/', DIRECTORY_SEPARATOR); |
|||
|
|||
self::$debugText .= "file-relative URI : {$uri}\n" |
|||
. "path prepended : {$path}\n"; |
|||
|
|||
// "unresolve" a symlink back to doc root |
|||
foreach ($symlinks as $link => $target) { |
|||
if (0 === strpos($path, $target)) { |
|||
// replace $target with $link |
|||
$path = $link . substr($path, strlen($target)); |
|||
|
|||
self::$debugText .= "symlink unresolved : {$path}\n"; |
|||
|
|||
break; |
|||
} |
|||
} |
|||
// strip doc root |
|||
$path = substr($path, strlen($realDocRoot)); |
|||
|
|||
self::$debugText .= "docroot stripped : {$path}\n"; |
|||
|
|||
// fix to root-relative URI |
|||
$uri = strtr($path, '/\\', '//'); |
|||
$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. |
|||
* |
|||
* @param string $path |
|||
* |
|||
* @return mixed path with no trailing slash |
|||
*/ |
|||
protected static function _realpath($path) |
|||
{ |
|||
$realPath = realpath($path); |
|||
if ($realPath !== false) { |
|||
$path = $realPath; |
|||
} |
|||
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})"; |
|||
} |
|||
} |
@ -0,0 +1,133 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Cache_APC |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* APC-based cache class for Minify |
|||
* |
|||
* <code> |
|||
* Minify::setCache(new Minify_Cache_APC()); |
|||
* </code> |
|||
* |
|||
* @package Minify |
|||
* @author Chris Edwards |
|||
**/ |
|||
class Minify_Cache_APC { |
|||
|
|||
/** |
|||
* Create a Minify_Cache_APC 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 apc_store($id, "{$_SERVER['REQUEST_TIME']}|{$data}", $this->_exp); |
|||
} |
|||
|
|||
/** |
|||
* Get the size of a cache entry |
|||
* |
|||
* @param string $id cache id |
|||
* |
|||
* @return int size in bytes |
|||
*/ |
|||
public function getSize($id) |
|||
{ |
|||
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); |
|||
} |
|||
|
|||
/** |
|||
* 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) |
|||
{ |
|||
return ($this->_fetch($id) && ($this->_lm >= $srcMtime)); |
|||
} |
|||
|
|||
/** |
|||
* 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 apc, store in instance |
|||
* |
|||
* @param string $id |
|||
* |
|||
* @return bool success |
|||
*/ |
|||
private function _fetch($id) |
|||
{ |
|||
if ($this->_id === $id) { |
|||
return true; |
|||
} |
|||
$ret = apc_fetch($id); |
|||
if (false === $ret) { |
|||
$this->_id = null; |
|||
return false; |
|||
} |
|||
list($this->_lm, $this->_data) = explode('|', $ret, 2); |
|||
$this->_id = $id; |
|||
return true; |
|||
} |
|||
} |
@ -0,0 +1,195 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Cache_File |
|||
* @package Minify |
|||
*/ |
|||
|
|||
class Minify_Cache_File { |
|||
|
|||
public function __construct($path = '', $fileLocking = false) |
|||
{ |
|||
if (! $path) { |
|||
$path = self::tmp(); |
|||
} |
|||
$this->_locking = $fileLocking; |
|||
$this->_path = $path; |
|||
} |
|||
|
|||
/** |
|||
* Write data to cache. |
|||
* |
|||
* @param string $id cache id (e.g. a filename) |
|||
* |
|||
* @param string $data |
|||
* |
|||
* @return bool success |
|||
*/ |
|||
public function store($id, $data) |
|||
{ |
|||
$flag = $this->_locking |
|||
? LOCK_EX |
|||
: null; |
|||
$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; |
|||
} |
|||
|
|||
/** |
|||
* Get the size of a cache entry |
|||
* |
|||
* @param string $id cache id (e.g. a filename) |
|||
* |
|||
* @return int size in bytes |
|||
*/ |
|||
public function getSize($id) |
|||
{ |
|||
return filesize($this->_path . '/' . $id); |
|||
} |
|||
|
|||
/** |
|||
* Does a valid cache entry exist? |
|||
* |
|||
* @param string $id cache id (e.g. a filename) |
|||
* |
|||
* @param int $srcMtime mtime of the original source file(s) |
|||
* |
|||
* @return bool exists |
|||
*/ |
|||
public function isValid($id, $srcMtime) |
|||
{ |
|||
$file = $this->_path . '/' . $id; |
|||
return (is_file($file) && (filemtime($file) >= $srcMtime)); |
|||
} |
|||
|
|||
/** |
|||
* Send the cached content to output |
|||
* |
|||
* @param string $id cache id (e.g. a filename) |
|||
*/ |
|||
public function display($id) |
|||
{ |
|||
if ($this->_locking) { |
|||
$fp = fopen($this->_path . '/' . $id, 'rb'); |
|||
flock($fp, LOCK_SH); |
|||
fpassthru($fp); |
|||
flock($fp, LOCK_UN); |
|||
fclose($fp); |
|||
} else { |
|||
readfile($this->_path . '/' . $id); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Fetch the cached content |
|||
* |
|||
* @param string $id cache id (e.g. a filename) |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function fetch($id) |
|||
{ |
|||
if ($this->_locking) { |
|||
$fp = fopen($this->_path . '/' . $id, 'rb'); |
|||
flock($fp, LOCK_SH); |
|||
$ret = stream_get_contents($fp); |
|||
flock($fp, LOCK_UN); |
|||
fclose($fp); |
|||
return $ret; |
|||
} else { |
|||
return file_get_contents($this->_path . '/' . $id); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Fetch the cache path used |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getPath() |
|||
{ |
|||
return $this->_path; |
|||
} |
|||
|
|||
/** |
|||
* Get a usable temp directory |
|||
* |
|||
* Adapted from Solar/Dir.php |
|||
* @author Paul M. Jones <pmjones@solarphp.com> |
|||
* @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 <pmjones@solarphp.com> |
|||
* @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; |
|||
} |
@ -0,0 +1,140 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Cache_Memcache |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Memcache-based cache class for Minify |
|||
* |
|||
* <code> |
|||
* // fall back to disk caching if memcache can't connect |
|||
* $memcache = new Memcache; |
|||
* if ($memcache->connect('localhost', 11211)) { |
|||
* Minify::setCache(new Minify_Cache_Memcache($memcache)); |
|||
* } else { |
|||
* Minify::setCache(); |
|||
* } |
|||
* </code> |
|||
**/ |
|||
class Minify_Cache_Memcache { |
|||
|
|||
/** |
|||
* Create a Minify_Cache_Memcache object, to be passed to |
|||
* Minify::setCache(). |
|||
* |
|||
* @param Memcache $memcache already-connected instance |
|||
* |
|||
* @param int $expire seconds until expiration (default = 0 |
|||
* meaning the item will not get an expiration date) |
|||
* |
|||
* @return null |
|||
*/ |
|||
public function __construct($memcache, $expire = 0) |
|||
{ |
|||
$this->_mc = $memcache; |
|||
$this->_exp = $expire; |
|||
} |
|||
|
|||
/** |
|||
* Write data to cache. |
|||
* |
|||
* @param string $id cache id |
|||
* |
|||
* @param string $data |
|||
* |
|||
* @return bool success |
|||
*/ |
|||
public function store($id, $data) |
|||
{ |
|||
return $this->_mc->set($id, "{$_SERVER['REQUEST_TIME']}|{$data}", 0, $this->_exp); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Get the size of a cache entry |
|||
* |
|||
* @param string $id cache id |
|||
* |
|||
* @return int size in bytes |
|||
*/ |
|||
public function getSize($id) |
|||
{ |
|||
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); |
|||
} |
|||
|
|||
/** |
|||
* 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) |
|||
{ |
|||
return ($this->_fetch($id) && ($this->_lm >= $srcMtime)); |
|||
} |
|||
|
|||
/** |
|||
* 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 $_mc = null; |
|||
private $_exp = null; |
|||
|
|||
// cache of most recently fetched id |
|||
private $_lm = null; |
|||
private $_data = null; |
|||
private $_id = null; |
|||
|
|||
/** |
|||
* Fetch data and timestamp from memcache, store in instance |
|||
* |
|||
* @param string $id |
|||
* |
|||
* @return bool success |
|||
*/ |
|||
private function _fetch($id) |
|||
{ |
|||
if ($this->_id === $id) { |
|||
return true; |
|||
} |
|||
$ret = $this->_mc->get($id); |
|||
if (false === $ret) { |
|||
$this->_id = null; |
|||
return false; |
|||
} |
|||
list($this->_lm, $this->_data) = explode('|', $ret, 2); |
|||
$this->_id = $id; |
|||
return true; |
|||
} |
|||
} |
@ -0,0 +1,142 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Cache_ZendPlatform |
|||
* @package Minify |
|||
*/ |
|||
|
|||
|
|||
/** |
|||
* ZendPlatform-based cache class for Minify |
|||
* |
|||
* Based on Minify_Cache_APC, uses output_cache_get/put (currently deprecated) |
|||
* |
|||
* <code> |
|||
* Minify::setCache(new Minify_Cache_ZendPlatform()); |
|||
* </code> |
|||
* |
|||
* @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; |
|||
} |
|||
} |
@ -0,0 +1,89 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_CommentPreserver |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Process a string in pieces preserving C-style comments that begin with "/*!" |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
class Minify_CommentPreserver { |
|||
|
|||
/** |
|||
* String to be prepended to each preserved comment |
|||
* |
|||
* @var string |
|||
*/ |
|||
public static $prepend = "\n"; |
|||
|
|||
/** |
|||
* String to be appended to each preserved comment |
|||
* |
|||
* @var string |
|||
*/ |
|||
public static $append = "\n"; |
|||
|
|||
/** |
|||
* 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 comments will be surrounded by |
|||
* Minify_CommentPreserver::$preprend and Minify_CommentPreserver::$append. |
|||
* |
|||
* @param string $content |
|||
* @param callback $processor function |
|||
* @param array $args array of extra arguments to pass to the processor |
|||
* function (default = array()) |
|||
* @return string |
|||
*/ |
|||
public static function process($content, $processor, $args = array()) |
|||
{ |
|||
$ret = ''; |
|||
while (true) { |
|||
list($beforeComment, $comment, $afterComment) = self::_nextComment($content); |
|||
if ('' !== $beforeComment) { |
|||
$callArgs = $args; |
|||
array_unshift($callArgs, $beforeComment); |
|||
$ret .= call_user_func_array($processor, $callArgs); |
|||
} |
|||
if (false === $comment) { |
|||
break; |
|||
} |
|||
$ret .= $comment; |
|||
$content = $afterComment; |
|||
} |
|||
return $ret; |
|||
} |
|||
|
|||
/** |
|||
* Extract comments that YUI Compressor preserves. |
|||
* |
|||
* @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 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. |
|||
*/ |
|||
private static function _nextComment($in) |
|||
{ |
|||
if ( |
|||
false === ($start = strpos($in, '/*!')) |
|||
|| false === ($end = strpos($in, '*/', $start + 3)) |
|||
) { |
|||
return array($in, false, false); |
|||
} |
|||
$ret = array( |
|||
substr($in, 0, $start) |
|||
,self::$prepend . '/*!' . substr($in, $start + 3, $end - $start - 1) . self::$append |
|||
); |
|||
$endChars = (strlen($in) - $end - 2); |
|||
$ret[] = (0 === $endChars) |
|||
? '' |
|||
: substr($in, -$endChars); |
|||
return $ret; |
|||
} |
|||
} |
@ -0,0 +1,250 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Controller_Base |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Base class for Minify controller |
|||
* |
|||
* The controller class validates a request and uses it to create sources |
|||
* for minification and set options like contentType. It's also responsible |
|||
* for loading minifier code upon request. |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
abstract class Minify_Controller_Base { |
|||
|
|||
/** |
|||
* Setup controller sources and set an needed options for Minify::source |
|||
* |
|||
* You must override this method in your subclass controller to set |
|||
* $this->sources. If the request is NOT valid, make sure $this->sources |
|||
* is left an empty array. Then strip any controller-specific options from |
|||
* $options and return it. To serve files, $this->sources must be an array of |
|||
* Minify_Source objects. |
|||
* |
|||
* @param array $options controller and Minify options |
|||
* |
|||
* @return array $options Minify::serve options |
|||
*/ |
|||
abstract public function setupSources($options); |
|||
|
|||
/** |
|||
* Get default Minify options for this controller. |
|||
* |
|||
* Override in subclass to change defaults |
|||
* |
|||
* @return array options for Minify |
|||
*/ |
|||
public function getDefaultMinifyOptions() { |
|||
return array( |
|||
'isPublic' => true |
|||
,'encodeOutput' => function_exists('gzdeflate') |
|||
,'encodeMethod' => null // determine later |
|||
,'encodeLevel' => 9 |
|||
,'minifierOptions' => array() // no minifier options |
|||
,'contentTypeCharset' => 'utf-8' |
|||
,'maxAge' => 1800 // 30 minutes |
|||
,'rewriteCssUris' => true |
|||
,'bubbleCssImports' => false |
|||
,'quiet' => false // serve() will send headers and output |
|||
,'debug' => false |
|||
|
|||
// 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 |
|||
// file to require to load preprocessor |
|||
,'postprocessorRequire' => null |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Get default minifiers for this controller. |
|||
* |
|||
* Override in subclass to change defaults |
|||
* |
|||
* @return array minifier callbacks for common types |
|||
*/ |
|||
public function getDefaultMinifers() { |
|||
$ret[Minify::TYPE_JS] = array('JSMin', 'minify'); |
|||
$ret[Minify::TYPE_CSS] = array('Minify_CSS', 'minify'); |
|||
$ret[Minify::TYPE_HTML] = array('Minify_HTML', 'minify'); |
|||
return $ret; |
|||
} |
|||
|
|||
/** |
|||
* Load any code necessary to execute the given minifier callback. |
|||
* |
|||
* The controller is responsible for loading minification code on demand |
|||
* via this method. This built-in function will only load classes for |
|||
* static method callbacks where the class isn't already defined. It uses |
|||
* the PEAR convention, so, given array('Jimmy_Minifier', 'minCss'), this |
|||
* function will include 'Jimmy/Minifier.php'. |
|||
* |
|||
* If you need code loaded on demand and this doesn't suit you, you'll need |
|||
* to override this function in your subclass. |
|||
* @see Minify_Controller_Page::loadMinifier() |
|||
* |
|||
* @param callback $minifierCallback callback of minifier function |
|||
* |
|||
* @return null |
|||
*/ |
|||
public function loadMinifier($minifierCallback) |
|||
{ |
|||
if (is_array($minifierCallback) |
|||
&& is_string($minifierCallback[0]) |
|||
&& !class_exists($minifierCallback[0], false)) { |
|||
|
|||
require str_replace('_', '/', $minifierCallback[0]) . '.php'; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Is a user-given file within an allowable directory, existing, |
|||
* and having an extension js/css/html/txt ? |
|||
* |
|||
* This is a convenience function for controllers that have to accept |
|||
* user-given paths |
|||
* |
|||
* @param string $file full file path (already processed by realpath()) |
|||
* |
|||
* @param array $safeDirs directories where files are safe to serve. Files can also |
|||
* be in subdirectories of these directories. |
|||
* |
|||
* @return bool file is safe |
|||
* |
|||
* @deprecated use checkAllowDirs, checkNotHidden instead |
|||
*/ |
|||
public static function _fileIsSafe($file, $safeDirs) |
|||
{ |
|||
$pathOk = false; |
|||
foreach ((array)$safeDirs as $safeDir) { |
|||
if (strpos($file, $safeDir) === 0) { |
|||
$pathOk = true; |
|||
break; |
|||
} |
|||
} |
|||
$base = basename($file); |
|||
if (! $pathOk || ! is_file($file) || $base[0] === '.') { |
|||
return false; |
|||
} |
|||
list($revExt) = explode('.', strrev($base)); |
|||
return in_array(strrev($revExt), array('js', 'css', 'html', 'txt')); |
|||
} |
|||
|
|||
/** |
|||
* @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 |
|||
* |
|||
* @param array $options user options |
|||
* |
|||
* @return array mixed options |
|||
*/ |
|||
public final function mixInDefaultOptions($options) |
|||
{ |
|||
$ret = array_merge( |
|||
$this->getDefaultMinifyOptions(), $options |
|||
); |
|||
if (! isset($options['minifiers'])) { |
|||
$options['minifiers'] = array(); |
|||
} |
|||
$ret['minifiers'] = array_merge( |
|||
$this->getDefaultMinifers(), $options['minifiers'] |
|||
); |
|||
return $ret; |
|||
} |
|||
|
|||
/** |
|||
* Analyze sources (if there are any) and set $options 'contentType' |
|||
* and 'lastModifiedTime' if they already aren't. |
|||
* |
|||
* @param array $options options for Minify |
|||
* |
|||
* @return array options for Minify |
|||
*/ |
|||
public final function analyzeSources($options = array()) |
|||
{ |
|||
if ($this->sources) { |
|||
if (! isset($options['contentType'])) { |
|||
$options['contentType'] = Minify_Source::getContentType($this->sources); |
|||
} |
|||
// last modified is needed for caching, even if setExpires is set |
|||
if (! isset($options['lastModifiedTime'])) { |
|||
$max = 0; |
|||
foreach ($this->sources as $source) { |
|||
$max = max($source->lastModified, $max); |
|||
} |
|||
$options['lastModifiedTime'] = $max; |
|||
} |
|||
} |
|||
return $options; |
|||
} |
|||
|
|||
/** |
|||
* Send message to the Minify logger |
|||
* |
|||
* @param string $msg |
|||
* |
|||
* @return null |
|||
*/ |
|||
public function log($msg) { |
|||
require_once 'Minify/Logger.php'; |
|||
Minify_Logger::log($msg); |
|||
} |
|||
} |
@ -0,0 +1,78 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Controller_Files |
|||
* @package Minify |
|||
*/ |
|||
|
|||
require_once 'Minify/Controller/Base.php'; |
|||
|
|||
/** |
|||
* Controller class for minifying a set of files |
|||
* |
|||
* E.g. the following would serve the minified Javascript for a site |
|||
* <code> |
|||
* Minify::serve('Files', array( |
|||
* 'files' => array( |
|||
* '//js/jquery.js' |
|||
* ,'//js/plugins.js' |
|||
* ,'/home/username/file.js' |
|||
* ) |
|||
* )); |
|||
* </code> |
|||
* |
|||
* As a shortcut, the controller will replace "//" at the beginning |
|||
* of a filename with $_SERVER['DOCUMENT_ROOT'] . '/'. |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
class Minify_Controller_Files extends Minify_Controller_Base { |
|||
|
|||
/** |
|||
* Set up file sources |
|||
* |
|||
* @param array $options controller and Minify options |
|||
* @return array Minify options |
|||
* |
|||
* Controller options: |
|||
* |
|||
* 'files': (required) array of complete file paths, or a single path |
|||
*/ |
|||
public function setupSources($options) { |
|||
// strip controller options |
|||
|
|||
$files = $options['files']; |
|||
// if $files is a single object, casting will break it |
|||
if (is_object($files)) { |
|||
$files = array($files); |
|||
} elseif (! is_array($files)) { |
|||
$files = (array)$files; |
|||
} |
|||
unset($options['files']); |
|||
|
|||
$sources = array(); |
|||
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 (is_file($realPath)) { |
|||
$sources[] = new Minify_Source(array( |
|||
'filepath' => $realPath |
|||
)); |
|||
} else { |
|||
$this->log("The path \"{$file}\" could not be found (or was not a file)"); |
|||
return $options; |
|||
} |
|||
} |
|||
if ($sources) { |
|||
$this->sources = $sources; |
|||
} |
|||
return $options; |
|||
} |
|||
} |
|||
|
@ -0,0 +1,93 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Controller_Groups |
|||
* @package Minify |
|||
*/ |
|||
|
|||
require_once 'Minify/Controller/Base.php'; |
|||
|
|||
/** |
|||
* Controller class for serving predetermined groups of minimized sets, selected |
|||
* by PATH_INFO |
|||
* |
|||
* <code> |
|||
* Minify::serve('Groups', array( |
|||
* 'groups' => array( |
|||
* 'css' => array('//css/type.css', '//css/layout.css') |
|||
* ,'js' => array('//js/jquery.js', '//js/site.js') |
|||
* ) |
|||
* )); |
|||
* </code> |
|||
* |
|||
* If the above code were placed in /serve.php, it would enable the URLs |
|||
* /serve.php/js and /serve.php/css |
|||
* |
|||
* As a shortcut, the controller will replace "//" at the beginning |
|||
* of a filename with $_SERVER['DOCUMENT_ROOT'] . '/'. |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
class Minify_Controller_Groups extends Minify_Controller_Base { |
|||
|
|||
/** |
|||
* Set up groups of files as sources |
|||
* |
|||
* @param array $options controller and Minify options |
|||
* |
|||
* 'groups': (required) array mapping PATH_INFO strings to arrays |
|||
* of complete file paths. @see Minify_Controller_Groups |
|||
* |
|||
* @return array Minify options |
|||
*/ |
|||
public function setupSources($options) { |
|||
// strip controller options |
|||
$groups = $options['groups']; |
|||
unset($options['groups']); |
|||
|
|||
// mod_fcgid places PATH_INFO in ORIG_PATH_INFO |
|||
$pi = isset($_SERVER['ORIG_PATH_INFO']) |
|||
? substr($_SERVER['ORIG_PATH_INFO'], 1) |
|||
: (isset($_SERVER['PATH_INFO']) |
|||
? substr($_SERVER['PATH_INFO'], 1) |
|||
: false |
|||
); |
|||
if (false === $pi || ! isset($groups[$pi])) { |
|||
// no PATH_INFO or not a valid group |
|||
$this->log("Missing PATH_INFO or no group set for \"$pi\""); |
|||
return $options; |
|||
} |
|||
$sources = array(); |
|||
|
|||
$files = $groups[$pi]; |
|||
// 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 (is_file($realPath)) { |
|||
$sources[] = new Minify_Source(array( |
|||
'filepath' => $realPath |
|||
)); |
|||
} else { |
|||
$this->log("The path \"{$file}\" could not be found (or was not a file)"); |
|||
return $options; |
|||
} |
|||
} |
|||
if ($sources) { |
|||
$this->sources = $sources; |
|||
} |
|||
return $options; |
|||
} |
|||
} |
|||
|
@ -0,0 +1,231 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Controller_MinApp |
|||
* @package Minify |
|||
*/ |
|||
|
|||
require_once 'Minify/Controller/Base.php'; |
|||
|
|||
/** |
|||
* Controller class for requests to /min/index.php |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
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 |
|||
$cOptions = array_merge( |
|||
array( |
|||
'allowDirs' => '//' |
|||
,'groupsOnly' => false |
|||
,'groups' => array() |
|||
,'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'])) { |
|||
// 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; |
|||
} |
|||
$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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (! $cOptions['groupsOnly'] && isset($_GET['f'])) { |
|||
// try user files |
|||
// The following restrictions are to limit the URLs that minify will |
|||
// 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'], $m) |
|||
// no "//" |
|||
|| strpos($_GET['f'], '//') !== false |
|||
// no "\" |
|||
|| strpos($_GET['f'], '\\') !== false |
|||
) { |
|||
$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 ($files != array_unique($files)) { |
|||
$this->log("Duplicate files were specified"); |
|||
return $options; |
|||
} |
|||
if (isset($_GET['b'])) { |
|||
// check for validity |
|||
if (preg_match('@^[^/]+(?:/[^/]+)*$@', $_GET['b']) |
|||
&& false === strpos($_GET['b'], '..') |
|||
&& $_GET['b'] !== '.') { |
|||
// valid base |
|||
$base = "/{$_GET['b']}/"; |
|||
} else { |
|||
$this->log("GET param 'b' was invalid"); |
|||
return $options; |
|||
} |
|||
} else { |
|||
$base = '/'; |
|||
} |
|||
$allowDirs = array(); |
|||
foreach ((array)$cOptions['allowDirs'] as $allowDir) { |
|||
$allowDirs[] = realpath(str_replace('//', $_SERVER['DOCUMENT_ROOT'] . '/', $allowDir)); |
|||
} |
|||
$basenames = array(); // just for cache id |
|||
foreach ($files as $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'); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,87 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Controller_Page |
|||
* @package Minify |
|||
*/ |
|||
|
|||
require_once 'Minify/Controller/Base.php'; |
|||
|
|||
/** |
|||
* Controller class for serving a single HTML page |
|||
* |
|||
* @link http://code.google.com/p/minify/source/browse/trunk/web/examples/1/index.php#59 |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
class Minify_Controller_Page extends Minify_Controller_Base { |
|||
|
|||
/** |
|||
* Set up source of HTML content |
|||
* |
|||
* @param array $options controller and Minify options |
|||
* @return array Minify options |
|||
* |
|||
* Controller options: |
|||
* |
|||
* 'content': (required) HTML markup |
|||
* |
|||
* 'id': (required) id of page (string for use in server-side caching) |
|||
* |
|||
* 'lastModifiedTime': timestamp of when this content changed. This |
|||
* is recommended to allow both server and client-side caching. |
|||
* |
|||
* 'minifyAll': should all CSS and Javascript blocks be individually |
|||
* minified? (default false) |
|||
* |
|||
* @todo Add 'file' option to read HTML file. |
|||
*/ |
|||
public function setupSources($options) { |
|||
if (isset($options['file'])) { |
|||
$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( |
|||
'cssMinifier' => array('Minify_CSS', 'minify') |
|||
,'jsMinifier' => array('JSMin', 'minify') |
|||
); |
|||
$this->_loadCssJsMinifiers = true; |
|||
unset($options['minifyAll']); |
|||
} |
|||
$this->sources[] = new Minify_Source($sourceSpec); |
|||
|
|||
$options['contentType'] = Minify::TYPE_HTML; |
|||
return $options; |
|||
} |
|||
|
|||
protected $_loadCssJsMinifiers = false; |
|||
|
|||
/** |
|||
* @see Minify_Controller_Base::loadMinifier() |
|||
*/ |
|||
public function loadMinifier($minifierCallback) |
|||
{ |
|||
if ($this->_loadCssJsMinifiers) { |
|||
// Minify will not call for these so we must manually load |
|||
// them when Minify/HTML.php is called for. |
|||
require_once 'Minify/CSS.php'; |
|||
require_once 'JSMin.php'; |
|||
} |
|||
parent::loadMinifier($minifierCallback); // load Minify/HTML.php |
|||
} |
|||
} |
|||
|
@ -0,0 +1,118 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Controller_Version1 |
|||
* @package Minify |
|||
*/ |
|||
|
|||
require_once 'Minify/Controller/Base.php'; |
|||
|
|||
/** |
|||
* Controller class for emulating version 1 of minify.php (mostly a proof-of-concept) |
|||
* |
|||
* <code> |
|||
* Minify::serve('Version1'); |
|||
* </code> |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
class Minify_Controller_Version1 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) { |
|||
self::_setupDefines(); |
|||
if (MINIFY_USE_CACHE) { |
|||
$cacheDir = defined('MINIFY_CACHE_DIR') |
|||
? MINIFY_CACHE_DIR |
|||
: ''; |
|||
Minify::setCache($cacheDir); |
|||
} |
|||
$options['badRequestHeader'] = 'HTTP/1.0 404 Not Found'; |
|||
$options['contentTypeCharset'] = MINIFY_ENCODING; |
|||
|
|||
// The following restrictions are to limit the URLs that minify will |
|||
// respond to. Ideally there should be only one way to reference a file. |
|||
if (! isset($_GET['files']) |
|||
// verify at least one file, files are single comma separated, |
|||
// and are all same extension |
|||
|| ! preg_match('/^[^,]+\\.(css|js)(,[^,]+\\.\\1)*$/', $_GET['files'], $m) |
|||
// no "//" (makes URL rewriting easier) |
|||
|| strpos($_GET['files'], '//') !== false |
|||
// no "\" |
|||
|| strpos($_GET['files'], '\\') !== false |
|||
// no "./" |
|||
|| preg_match('/(?:^|[^\\.])\\.\\//', $_GET['files']) |
|||
) { |
|||
return $options; |
|||
} |
|||
$extension = $m[1]; |
|||
|
|||
$files = explode(',', $_GET['files']); |
|||
if (count($files) > MINIFY_MAX_FILES) { |
|||
return $options; |
|||
} |
|||
|
|||
// strings for prepending to relative/absolute paths |
|||
$prependRelPaths = dirname($_SERVER['SCRIPT_FILENAME']) |
|||
. DIRECTORY_SEPARATOR; |
|||
$prependAbsPaths = $_SERVER['DOCUMENT_ROOT']; |
|||
|
|||
$sources = array(); |
|||
$goodFiles = array(); |
|||
$hasBadSource = false; |
|||
|
|||
$allowDirs = isset($options['allowDirs']) |
|||
? $options['allowDirs'] |
|||
: MINIFY_BASE_DIR; |
|||
|
|||
foreach ($files as $file) { |
|||
// prepend appropriate string for abs/rel paths |
|||
$file = ($file[0] === '/' ? $prependAbsPaths : $prependRelPaths) . $file; |
|||
// make sure a real file! |
|||
$file = realpath($file); |
|||
// don't allow unsafe or duplicate files |
|||
if (parent::_fileIsSafe($file, $allowDirs) |
|||
&& !in_array($file, $goodFiles)) |
|||
{ |
|||
$goodFiles[] = $file; |
|||
$srcOptions = array( |
|||
'filepath' => $file |
|||
); |
|||
$this->sources[] = new Minify_Source($srcOptions); |
|||
} else { |
|||
$hasBadSource = true; |
|||
break; |
|||
} |
|||
} |
|||
if ($hasBadSource) { |
|||
$this->sources = array(); |
|||
} |
|||
if (! MINIFY_REWRITE_CSS_URLS) { |
|||
$options['rewriteCssUris'] = false; |
|||
} |
|||
return $options; |
|||
} |
|||
|
|||
private static function _setupDefines() |
|||
{ |
|||
$defaults = array( |
|||
'MINIFY_BASE_DIR' => realpath($_SERVER['DOCUMENT_ROOT']) |
|||
,'MINIFY_ENCODING' => 'utf-8' |
|||
,'MINIFY_MAX_FILES' => 16 |
|||
,'MINIFY_REWRITE_CSS_URLS' => true |
|||
,'MINIFY_USE_CACHE' => true |
|||
); |
|||
foreach ($defaults as $const => $val) { |
|||
if (! defined($const)) { |
|||
define($const, $val); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
@ -0,0 +1,26 @@ |
|||
<?php |
|||
|
|||
/** |
|||
* Detect whether request should be debugged |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
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; |
|||
} |
|||
} |
@ -0,0 +1,246 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_HTML |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Compress HTML |
|||
* |
|||
* This is a heavy regex-based removal of whitespace, unnecessary comments and |
|||
* tokens. IE conditional comments are preserved. There are also options to have |
|||
* STYLE and SCRIPT blocks compressed by callback functions. |
|||
* |
|||
* A test suite is available. |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
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, '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML')); |
|||
} |
|||
|
|||
$this->_replacementHash = 'MINIFYHTML' . md5($_SERVER['REQUEST_TIME']); |
|||
$this->_placeholders = array(); |
|||
|
|||
// replace SCRIPTs (and minify) with placeholders |
|||
$this->_html = preg_replace_callback( |
|||
'/(\\s*)<script(\\b[^>]*?>)([\\s\\S]*?)<\\/script>(\\s*)/i' |
|||
,array($this, '_removeScriptCB') |
|||
,$this->_html); |
|||
|
|||
// replace STYLEs (and minify) with placeholders |
|||
$this->_html = preg_replace_callback( |
|||
'/\\s*<style(\\b[^>]*>)([\\s\\S]*?)<\\/style>\\s*/i' |
|||
,array($this, '_removeStyleCB') |
|||
,$this->_html); |
|||
|
|||
// remove HTML comments (not containing IE conditional comments). |
|||
$this->_html = preg_replace_callback( |
|||
'/<!--([\\s\\S]*?)-->/' |
|||
,array($this, '_commentCB') |
|||
,$this->_html); |
|||
|
|||
// replace PREs with placeholders |
|||
$this->_html = preg_replace_callback('/\\s*<pre(\\b[^>]*?>[\\s\\S]*?<\\/pre>)\\s*/i' |
|||
,array($this, '_removePreCB') |
|||
,$this->_html); |
|||
|
|||
// replace TEXTAREAs with placeholders |
|||
$this->_html = preg_replace_callback( |
|||
'/\\s*<textarea(\\b[^>]*?>[\\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], '<![')) |
|||
? $m[0] |
|||
: ''; |
|||
} |
|||
|
|||
protected function _reservePlace($content) |
|||
{ |
|||
$placeholder = '%' . $this->_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("<pre{$m[1]}"); |
|||
} |
|||
|
|||
protected function _removeTextareaCB($m) |
|||
{ |
|||
return $this->_reservePlace("<textarea{$m[1]}"); |
|||
} |
|||
|
|||
protected function _removeStyleCB($m) |
|||
{ |
|||
$openStyle = "<style{$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}/*<![CDATA[*/{$css}/*]]>*/</style>" |
|||
: "{$openStyle}{$css}</style>" |
|||
); |
|||
} |
|||
|
|||
protected function _removeScriptCB($m) |
|||
{ |
|||
$openScript = "<script{$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*|\\s*(?:\\/\\/)?\\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}/*<![CDATA[*/{$js}/*]]>*/</script>{$ws2}" |
|||
: "{$ws1}{$openScript}{$js}</script>{$ws2}" |
|||
); |
|||
} |
|||
|
|||
protected function _removeCdata($str) |
|||
{ |
|||
return (false !== strpos($str, '<![CDATA[')) |
|||
? str_replace(array('<![CDATA[', ']]>'), '', $str) |
|||
: $str; |
|||
} |
|||
|
|||
protected function _needsCdata($str) |
|||
{ |
|||
return ($this->_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str)); |
|||
} |
|||
} |
@ -0,0 +1,193 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_HTML_Helper |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Helpers for writing Minfy URIs into HTML |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
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=<paths> |
|||
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; |
|||
} |
|||
} |
@ -0,0 +1,216 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_ImportProcessor |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Linearize a CSS/JS file by including content specified by CSS import |
|||
* declarations. In CSS files, relative URIs are fixed. |
|||
* |
|||
* @imports will be processed regardless of where they appear in the source |
|||
* files; i.e. @imports commented out or in string content will still be |
|||
* processed! |
|||
* |
|||
* This has a unit test but should be considered "experimental". |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
* @author Simon Schick <simonsimcity@gmail.com> |
|||
*/ |
|||
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; |
|||
} |
|||
} |
@ -0,0 +1,133 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_JS_ClosureCompiler |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Minify Javascript using Google's Closure Compiler API |
|||
* |
|||
* @link http://code.google.com/closure/compiler/ |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
* |
|||
* @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 {} |
@ -0,0 +1,137 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Lines |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Add line numbers in C-style comments for easier debugging of combined content |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
* @author Adam Pedersen (Issue 55 fix) |
|||
*/ |
|||
class Minify_Lines { |
|||
|
|||
/** |
|||
* Add line numbers in C-style comments |
|||
* |
|||
* This uses a very basic parser easily fooled by comment tokens inside |
|||
* strings or regexes, but, otherwise, generally clean code will not be |
|||
* mangled. URI rewriting can also be performed. |
|||
* |
|||
* @param string $content |
|||
* |
|||
* @param array $options available options: |
|||
* |
|||
* 'id': (optional) string to identify file. E.g. file name/path |
|||
* |
|||
* '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, and prepend a comment with debugging information about |
|||
* this process. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public static function minify($content, $options = array()) |
|||
{ |
|||
$id = (isset($options['id']) && $options['id']) |
|||
? $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((string) $numLines); // e.g. 103 lines = 3 digits |
|||
$inComment = false; |
|||
$i = 0; |
|||
$newLines = array(); |
|||
while (null !== ($line = array_shift($lines))) { |
|||
if (('' !== $id) && (0 == $i % 50)) { |
|||
array_push($newLines, '', "/* {$id} */", ''); |
|||
} |
|||
++$i; |
|||
$newLines[] = self::_addNote($line, $i, $inComment, $padTo); |
|||
$inComment = self::_eolInComment($line, $inComment); |
|||
} |
|||
$content = implode("\n", $newLines) . "\n"; |
|||
|
|||
// check for desired URI rewriting |
|||
if (isset($options['currentDir'])) { |
|||
require_once 'Minify/CSS/UriRewriter.php'; |
|||
Minify_CSS_UriRewriter::$debugText = ''; |
|||
$content = Minify_CSS_UriRewriter::rewrite( |
|||
$content |
|||
,$options['currentDir'] |
|||
,isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT'] |
|||
,isset($options['symlinks']) ? $options['symlinks'] : array() |
|||
); |
|||
$content = "/* Minify_CSS_UriRewriter::\$debugText\n\n" |
|||
. Minify_CSS_UriRewriter::$debugText . "*/\n" |
|||
. $content; |
|||
} |
|||
|
|||
return $content; |
|||
} |
|||
|
|||
/** |
|||
* Is the parser within a C-style comment at the end of this line? |
|||
* |
|||
* @param string $line current line of code |
|||
* |
|||
* @param bool $inComment was the parser in a comment at the |
|||
* beginning of the line? |
|||
* |
|||
* @return bool |
|||
*/ |
|||
private static function _eolInComment($line, $inComment) |
|||
{ |
|||
while (strlen($line)) { |
|||
$search = $inComment |
|||
? '*/' |
|||
: '/*'; |
|||
$pos = strpos($line, $search); |
|||
if (false === $pos) { |
|||
return $inComment; |
|||
} else { |
|||
if ($pos == 0 |
|||
|| ($inComment |
|||
? substr($line, $pos, 3) |
|||
: substr($line, $pos-1, 3)) != '*/*') |
|||
{ |
|||
$inComment = ! $inComment; |
|||
} |
|||
$line = substr($line, $pos + 2); |
|||
} |
|||
} |
|||
return $inComment; |
|||
} |
|||
|
|||
/** |
|||
* Prepend a comment (or note) to the given line |
|||
* |
|||
* @param string $line current line of code |
|||
* |
|||
* @param string $note content of note/comment |
|||
* |
|||
* @param bool $inComment was the parser in a comment at the |
|||
* beginning of the line? |
|||
* |
|||
* @param int $padTo minimum width of comment |
|||
* |
|||
* @return string |
|||
*/ |
|||
private static function _addNote($line, $note, $inComment, $padTo) |
|||
{ |
|||
return $inComment |
|||
? '/* ' . str_pad($note, $padTo, ' ', STR_PAD_RIGHT) . ' *| ' . $line |
|||
: '/* ' . str_pad($note, $padTo, ' ', STR_PAD_RIGHT) . ' */ ' . $line; |
|||
} |
|||
} |
@ -0,0 +1,47 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Logger |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Message logging class |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
* |
|||
* @todo lose this singleton! pass log object in Minify::serve and distribute to others |
|||
*/ |
|||
class Minify_Logger { |
|||
|
|||
/** |
|||
* Set logger object. |
|||
* |
|||
* The object should have a method "log" that accepts a value as 1st argument and |
|||
* an optional string label as the 2nd. |
|||
* |
|||
* @param mixed $obj or a "falsey" value to disable |
|||
* @return null |
|||
*/ |
|||
public static function setLogger($obj = null) { |
|||
self::$_logger = $obj |
|||
? $obj |
|||
: null; |
|||
} |
|||
|
|||
/** |
|||
* Pass a message to the logger (if set) |
|||
* |
|||
* @param string $msg message to log |
|||
* @return null |
|||
*/ |
|||
public static function log($msg, $label = 'Minify') { |
|||
if (! self::$_logger) return; |
|||
self::$_logger->log($msg, $label); |
|||
} |
|||
|
|||
/** |
|||
* @var mixed logger object (like FirePHP) or null (i.e. no logger available) |
|||
*/ |
|||
private static $_logger = null; |
|||
} |
@ -0,0 +1,37 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Packer |
|||
* |
|||
* To use this class you must first download the PHP port of Packer |
|||
* and place the file "class.JavaScriptPacker.php" in /lib (or your |
|||
* include_path). |
|||
* @link http://joliclic.free.fr/php/javascript-packer/en/ |
|||
* |
|||
* Be aware that, as long as HTTP encoding is used, scripts minified with JSMin |
|||
* will provide better client-side performance, as they need not be unpacked in |
|||
* client-side code. |
|||
* |
|||
* @package Minify |
|||
*/ |
|||
|
|||
if (false === (@include 'class.JavaScriptPacker.php')) { |
|||
trigger_error( |
|||
'The script "class.JavaScriptPacker.php" is required. Please see: http:' |
|||
.'//code.google.com/p/minify/source/browse/trunk/min/lib/Minify/Packer.php' |
|||
,E_USER_ERROR |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Minify Javascript using Dean Edward's Packer |
|||
* |
|||
* @package Minify |
|||
*/ |
|||
class Minify_Packer { |
|||
public static function minify($code, $options = array()) |
|||
{ |
|||
// @todo: set encoding options based on $options :) |
|||
$packer = new JavascriptPacker($code, 'Normal', true, false); |
|||
return trim($packer->pack()); |
|||
} |
|||
} |
@ -0,0 +1,187 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_Source |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* A content source to be minified by Minify. |
|||
* |
|||
* This allows per-source minification options and the mixing of files with |
|||
* content from other sources. |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
class Minify_Source { |
|||
|
|||
/** |
|||
* @var int time of last modification |
|||
*/ |
|||
public $lastModified = null; |
|||
|
|||
/** |
|||
* @var callback minifier function specifically for this source. |
|||
*/ |
|||
public $minifier = null; |
|||
|
|||
/** |
|||
* @var array minification options specific to this source. |
|||
*/ |
|||
public $minifyOptions = null; |
|||
|
|||
/** |
|||
* @var string full path of file |
|||
*/ |
|||
public $filepath = null; |
|||
|
|||
/** |
|||
* @var string HTTP Content Type (Minify requires one of the constants Minify::TYPE_*) |
|||
*/ |
|||
public $contentType = null; |
|||
|
|||
/** |
|||
* Create a Minify_Source |
|||
* |
|||
* In the $spec array(), you can either provide a 'filepath' to an existing |
|||
* file (existence will not be checked!) or give 'id' (unique string for |
|||
* the content), 'content' (the string content) and 'lastModified' |
|||
* (unixtime of last update). |
|||
* |
|||
* As a shortcut, the controller will replace "//" at the beginning |
|||
* of a filepath with $_SERVER['DOCUMENT_ROOT'] . '/'. |
|||
* |
|||
* @param array $spec options |
|||
*/ |
|||
public function __construct($spec) |
|||
{ |
|||
if (isset($spec['filepath'])) { |
|||
if (0 === strpos($spec['filepath'], '//')) { |
|||
$spec['filepath'] = $_SERVER['DOCUMENT_ROOT'] . substr($spec['filepath'], 1); |
|||
} |
|||
$segments = explode('.', $spec['filepath']); |
|||
$ext = strtolower(array_pop($segments)); |
|||
switch ($ext) { |
|||
case 'js' : $this->contentType = 'application/x-javascript'; |
|||
break; |
|||
case 'css' : $this->contentType = 'text/css'; |
|||
break; |
|||
case 'htm' : // fallthrough |
|||
case 'html' : $this->contentType = 'text/html'; |
|||
break; |
|||
} |
|||
$this->filepath = $spec['filepath']; |
|||
$this->_id = $spec['filepath']; |
|||
$this->lastModified = filemtime($spec['filepath']) |
|||
// offset for Windows uploaders with out of sync clocks |
|||
+ round(Minify::$uploaderHoursBehind * 3600); |
|||
} elseif (isset($spec['id'])) { |
|||
$this->_id = 'id::' . $spec['id']; |
|||
if (isset($spec['content'])) { |
|||
$this->_content = $spec['content']; |
|||
} else { |
|||
$this->_getContentFunc = $spec['getContentFunc']; |
|||
} |
|||
$this->lastModified = isset($spec['lastModified']) |
|||
? $spec['lastModified'] |
|||
: time(); |
|||
} |
|||
if (isset($spec['contentType'])) { |
|||
$this->contentType = $spec['contentType']; |
|||
} |
|||
if (isset($spec['minifier'])) { |
|||
$this->minifier = $spec['minifier']; |
|||
} |
|||
if (isset($spec['minifyOptions'])) { |
|||
$this->minifyOptions = $spec['minifyOptions']; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get content |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getContent() |
|||
{ |
|||
$content = (null !== $this->filepath) |
|||
? file_get_contents($this->filepath) |
|||
: ((null !== $this->_content) |
|||
? $this->_content |
|||
: call_user_func($this->_getContentFunc, $this->_id) |
|||
); |
|||
// remove UTF-8 BOM if present |
|||
return (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) |
|||
? substr($content, 3) |
|||
: $content; |
|||
} |
|||
|
|||
/** |
|||
* Get id |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getId() |
|||
{ |
|||
return $this->_id; |
|||
} |
|||
|
|||
/** |
|||
* Verifies a single minification call can handle all sources |
|||
* |
|||
* @param array $sources Minify_Source instances |
|||
* |
|||
* @return bool true iff there no sources with specific minifier preferences. |
|||
*/ |
|||
public static function haveNoMinifyPrefs($sources) |
|||
{ |
|||
foreach ($sources as $source) { |
|||
if (null !== $source->minifier |
|||
|| null !== $source->minifyOptions) { |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* Get unique string for a set of sources |
|||
* |
|||
* @param array $sources Minify_Source instances |
|||
* |
|||
* @return string |
|||
*/ |
|||
public static function getDigest($sources) |
|||
{ |
|||
foreach ($sources as $source) { |
|||
$info[] = array( |
|||
$source->_id, $source->minifier, $source->minifyOptions |
|||
); |
|||
} |
|||
return md5(serialize($info)); |
|||
} |
|||
|
|||
/** |
|||
* Get content type from a group of sources |
|||
* |
|||
* This is called if the user doesn't pass in a 'contentType' options |
|||
* |
|||
* @param array $sources Minify_Source instances |
|||
* |
|||
* @return string content type. e.g. 'text/css' |
|||
*/ |
|||
public static function getContentType($sources) |
|||
{ |
|||
foreach ($sources as $source) { |
|||
if ($source->contentType !== null) { |
|||
return $source->contentType; |
|||
} |
|||
} |
|||
return 'text/plain'; |
|||
} |
|||
|
|||
protected $_content = null; |
|||
protected $_getContentFunc = null; |
|||
protected $_id = null; |
|||
} |
|||
|
@ -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); |
|||
} |
|||
} |
@ -0,0 +1,171 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_YUI_CssCompressor |
|||
* @package Minify |
|||
* |
|||
* YUI Compressor |
|||
* Author: Julien Lecomte - http://www.julienlecomte.net/ |
|||
* Author: Isaac Schlueter - http://foohack.com/ |
|||
* Author: Stoyan Stefanov - http://phpied.com/ |
|||
* Author: Steve Clay - http://www.mrclay.org/ (PHP port) |
|||
* Copyright (c) 2009 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. |
|||
*/ |
|||
|
|||
/** |
|||
* Compress CSS (incomplete DO NOT USE) |
|||
* |
|||
* @see https://github.com/yui/yuicompressor/blob/master/src/com/yahoo/platform/yui/compressor/CssCompressor.java |
|||
* |
|||
* @package Minify |
|||
*/ |
|||
class Minify_YUI_CssCompressor { |
|||
|
|||
/** |
|||
* Minify a CSS string |
|||
* |
|||
* @param string $css |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function compress($css, $linebreakpos = 0) |
|||
{ |
|||
$css = str_replace("\r\n", "\n", $css); |
|||
|
|||
/** |
|||
* @todo comment removal |
|||
* @todo re-port from newer Java version |
|||
*/ |
|||
|
|||
// Normalize all whitespace strings to single spaces. Easier to work with that way. |
|||
$css = preg_replace('@\s+@', ' ', $css); |
|||
|
|||
// Make a pseudo class for the Box Model Hack |
|||
$css = preg_replace("@\"\\\\\"}\\\\\"\"@", "___PSEUDOCLASSBMH___", $css); |
|||
|
|||
// 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. |
|||
$css = preg_replace_callback("@(^|\\})(([^\\{:])+:)+([^\\{]*\\{)@", array($this, '_removeSpacesCB'), $css); |
|||
|
|||
$css = preg_replace("@\\s+([!{};:>+\\(\\)\\],])@", "$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]; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,148 @@ |
|||
<?php |
|||
/** |
|||
* Class Minify_YUICompressor |
|||
* @package Minify |
|||
*/ |
|||
|
|||
/** |
|||
* Compress Javascript/CSS using the YUI Compressor |
|||
* |
|||
* You must set $jarFile and $tempDir before calling the minify functions. |
|||
* Also, depending on your shell's environment, you may need to specify |
|||
* the full path to java in $javaExecutable or use putenv() to setup the |
|||
* Java environment. |
|||
* |
|||
* <code> |
|||
* Minify_YUICompressor::$jarFile = '/path/to/yuicompressor-2.3.5.jar'; |
|||
* Minify_YUICompressor::$tempDir = '/tmp'; |
|||
* $code = Minify_YUICompressor::minifyJs( |
|||
* $code |
|||
* ,array('nomunge' => true, 'line-break' => 1000) |
|||
* ); |
|||
* </code> |
|||
* |
|||
* @todo unit tests, $options docs |
|||
* |
|||
* @package Minify |
|||
* @author Stephen Clay <steve@mrclay.org> |
|||
*/ |
|||
class Minify_YUICompressor { |
|||
|
|||
/** |
|||
* Filepath of the YUI Compressor jar file. This must be set before |
|||
* calling minifyJs() or minifyCss(). |
|||
* |
|||
* @var string |
|||
*/ |
|||
public static $jarFile = null; |
|||
|
|||
/** |
|||
* Writable temp directory. This must be set before calling minifyJs() |
|||
* or minifyCss(). |
|||
* |
|||
* @var string |
|||
*/ |
|||
public static $tempDir = null; |
|||
|
|||
/** |
|||
* Filepath of "java" executable (may be needed if not in shell's PATH) |
|||
* |
|||
* @var string |
|||
*/ |
|||
public static $javaExecutable = 'java'; |
|||
|
|||
/** |
|||
* Minify a Javascript string |
|||
* |
|||
* @param string $js |
|||
* |
|||
* @param array $options (verbose is ignored) |
|||
* |
|||
* @see http://www.julienlecomte.net/yuicompressor/README |
|||
* |
|||
* @return string |
|||
*/ |
|||
public static function minifyJs($js, $options = array()) |
|||
{ |
|||
return self::_minify('js', $js, $options); |
|||
} |
|||
|
|||
/** |
|||
* Minify a CSS string |
|||
* |
|||
* @param string $css |
|||
* |
|||
* @param array $options (verbose is ignored) |
|||
* |
|||
* @see http://www.julienlecomte.net/yuicompressor/README |
|||
* |
|||
* @return string |
|||
*/ |
|||
public static function minifyCss($css, $options = array()) |
|||
{ |
|||
return self::_minify('css', $css, $options); |
|||
} |
|||
|
|||
private static function _minify($type, $content, $options) |
|||
{ |
|||
self::_prepare(); |
|||
if (! ($tmpFile = tempnam(self::$tempDir, 'yuic_'))) { |
|||
throw new Exception('Minify_YUICompressor : could not create temp file.'); |
|||
} |
|||
file_put_contents($tmpFile, $content); |
|||
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); |
|||
} |
|||
|
|||
private static function _getCmd($userOptions, $type, $tmpFile) |
|||
{ |
|||
$o = array_merge( |
|||
array( |
|||
'charset' => '' |
|||
,'line-break' => 5000 |
|||
,'type' => $type |
|||
,'nomunge' => false |
|||
,'preserve-semi' => false |
|||
,'disable-optimizations' => false |
|||
) |
|||
,$userOptions |
|||
); |
|||
$cmd = self::$javaExecutable . ' -jar ' . escapeshellarg(self::$jarFile) |
|||
. " --type {$type}" |
|||
. (preg_match('/^[\\da-zA-Z0-9\\-]+$/', $o['charset']) |
|||
? " --charset {$o['charset']}" |
|||
: '') |
|||
. (is_numeric($o['line-break']) && $o['line-break'] >= 0 |
|||
? ' --line-break ' . (int)$o['line-break'] |
|||
: ''); |
|||
if ($type === 'js') { |
|||
foreach (array('nomunge', 'preserve-semi', 'disable-optimizations') as $opt) { |
|||
$cmd .= $o[$opt] |
|||
? " --{$opt}" |
|||
: ''; |
|||
} |
|||
} |
|||
return $cmd . ' ' . escapeshellarg($tmpFile); |
|||
} |
|||
|
|||
private static function _prepare() |
|||
{ |
|||
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.'); |
|||
} |
|||
} |
|||
} |
|||
|
@ -0,0 +1,381 @@ |
|||
<?php |
|||
|
|||
namespace MrClay; |
|||
|
|||
/** |
|||
* Forms a front controller for a console app, handling and validating arguments (options) |
|||
* |
|||
* Instantiate, add arguments, then call validate(). Afterwards, the user's valid arguments |
|||
* and their values will be available in $cli->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 <steve@mrclay.org> |
|||
* @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); |
|||
} |
|||
} |
|||
|
@ -0,0 +1,181 @@ |
|||
<?php |
|||
|
|||
namespace MrClay\Cli; |
|||
|
|||
/** |
|||
* An argument for a CLI app. This specifies the argument, what values it expects and |
|||
* how it's treated during validation. |
|||
* |
|||
* By default, the argument will be assumed to be an optional letter flag with no value following. |
|||
* |
|||
* If the argument may receive a value, call mayHaveValue(). If there's whitespace after the |
|||
* flag, the value will be returned as true instead of the string. |
|||
* |
|||
* If the argument MUST be accompanied by a value, call mustHaveValue(). In this case, whitespace |
|||
* is permitted between the flag and its value. |
|||
* |
|||
* Use assertFile() or assertDir() to indicate that the argument must return a string value |
|||
* specifying a file or directory. During validation, the value will be resolved to a |
|||
* full file/dir path (not necessarily existing!) and the original value will be accessible |
|||
* via a "*.raw" key. E.g. $cli->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 <steve@mrclay.org> |
|||
* @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; |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
<div class="delete"> |
|||
{% trans %}Delete Post{% endtrans %} [<input title="Delete file only" type="checkbox" name="file" id="delete_file" /> |
|||
<label for="delete_file">{% trans %}File{% endtrans %}</label>] <label for="password">{% trans %}Password{% endtrans %}</label> |
|||
<input id="password" type="password" name="password" size="12" maxlength="18" /> |
|||
<input type="submit" name="delete" value="{% trans %}Delete{% endtrans %}" /> |
|||
</div> |
|||
<div class="delete" style="clear:both"> |
|||
<label for="reason">{% trans %}Reason{% endtrans %}</label> |
|||
<input id="reason" type="text" name="reason" size="20" maxlength="30" /> |
|||
<input type="submit" name="report" value="{% trans %}Report{% endtrans %}" /> |
|||
</div> |
Loading…
Reference in new issue