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