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
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'], |
|||