1
0
mirror of https://github.com/mrclay/minify.git synced 2025-08-08 23:26:43 +02:00

Perf update for JSmin (Minify_Javascript), + phpDocs for public HTTP_* class APIs, minor Minify fix, private members changed to protected to allow easier subclassing.

This commit is contained in:
Steve Clay
2008-03-03 16:23:39 +00:00
parent 7a3d7129b4
commit e8ac1dc8d0
9 changed files with 304 additions and 161 deletions

View File

@@ -32,49 +32,43 @@
*/
class HTTP_ConditionalGet {
private $headers = array();
private $lmTime = null;
private $etag = null;
/**
* 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;
public function getHeaders() {
return $this->headers;
}
/**
* Depending on the PHP config, PHP will buffer all output and set
* Content-Length for you. If it doesn't, or you flush() while sending data,
* you'll want to call this to let the client know up front.
* @param array $spec options
*
* 'isPublic': (bool) if true, the Cache-Control header will contain
* "public", allowing proxy caches to cache the content. Otherwise
* "private" will be sent, allowing only browsers to cache. (default false)
*
* 'lastModifiedTime': (int) if given, both ETag AND Last-Modified headers
* will be sent with content. This is recommended.
*
* '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.
*
* 'invalidate': (bool) if true, the client cache will be considered invalid
* without testing. Effectively this disables conditional GET.
* (default false)
*
* 'setExpires': (mixed) set this to a timestamp or GMT date to send an
* Expires header with the content instead of ETag/Last-Modified. If given,
* Conditional GETs will not be performed, but the public/private
* Cache-Control header will still be sent. (default null)
*
* @return null
*/
public function setContentLength($bytes) {
return $this->headers['Content-Length'] = $bytes;
}
public function sendHeaders() {
$headers = $this->headers;
if (array_key_exists('_responseCode', $headers)) {
header($headers['_responseCode']);
unset($headers['_responseCode']);
}
foreach ($headers as $name => $val) {
header($name . ': ' . $val);
}
}
private function setEtag($hash, $scope) {
$this->etag = '"' . $hash
. substr($scope, 0, 3)
. '"';
$this->headers['ETag'] = $this->etag;
}
private function setLastModified($time) {
$this->lmTime = (int)$time;
$this->headers['Last-Modified'] = self::gmtdate($time);
}
// TODO: allow custom Cache-Control directives, but offer pre-configured
// "modes" for common cache models
public function __construct($spec) {
$scope = (isset($spec['isPublic']) && $spec['isPublic'])
? 'public'
@@ -84,7 +78,7 @@ class HTTP_ConditionalGet {
if (is_numeric($spec['setExpires'])) {
$spec['setExpires'] = self::gmtdate($spec['setExpires']);
}
$this->headers = array(
$this->_headers = array(
'Cache-Control' => $scope
,'Expires' => $spec['setExpires']
);
@@ -93,41 +87,114 @@ class HTTP_ConditionalGet {
}
if (isset($spec['lastModifiedTime'])) {
// base both headers on time
$this->setLastModified($spec['lastModifiedTime']);
$this->setEtag($spec['lastModifiedTime'], $scope);
$this->_setLastModified($spec['lastModifiedTime']);
$this->_setEtag($spec['lastModifiedTime'], $scope);
} else {
// hope to use ETag
if (isset($spec['contentHash'])) {
$this->setEtag($spec['contentHash'], $scope);
$this->_setEtag($spec['contentHash'], $scope);
}
}
$this->headers['Cache-Control'] = "max-age=0, {$scope}, must-revalidate";
$this->_headers['Cache-Control'] = "max-age=0, {$scope}, must-revalidate";
// invalidate cache if disabled, otherwise check
$this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate'])
? false
: $this->isCacheValid();
: $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, must-revalidate'
* ,'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)) {
header($headers['_responseCode']);
unset($headers['_responseCode']);
}
foreach ($headers as $name => $val) {
header($name . ': ' . $val);
}
}
protected $_headers = array();
protected $_lmTime = null;
protected $_etag = null;
protected function _setEtag($hash, $scope) {
$this->_etag = '"' . $hash
. substr($scope, 0, 3)
. '"';
$this->_headers['ETag'] = $this->_etag;
}
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
*/
private function isCacheValid()
protected function _isCacheValid()
{
if (null === $this->etag) {
if (null === $this->_etag) {
// ETag was our backup, so we know we don't have lmTime either
return false;
}
$isValid = ($this->resourceMatchedEtag() || $this->resourceNotModified());
if ($isValid) {
// overwrite headers, only need 304
$this->headers = array(
$this->_headers = array(
'_responseCode' => 'HTTP/1.0 304 Not Modified'
);
}
return $isValid;
}
private function resourceMatchedEtag() {
protected function resourceMatchedEtag() {
if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
return false;
}
@@ -136,14 +203,14 @@ class HTTP_ConditionalGet {
: $_SERVER['HTTP_IF_NONE_MATCH'];
$cachedEtags = split(',', $cachedEtagList);
foreach ($cachedEtags as $cachedEtag) {
if (trim($cachedEtag) == $this->etag) {
if (trim($cachedEtag) == $this->_etag) {
return true;
}
}
return false;
}
private function resourceNotModified() {
protected function resourceNotModified() {
if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
return false;
}
@@ -154,11 +221,10 @@ class HTTP_ConditionalGet {
// IE has tacked on extra data to this header, strip it
$ifModifiedSince = substr($ifModifiedSince, 0, $semicolon);
}
return ($ifModifiedSince == self::gmtdate($this->lmTime));
return ($ifModifiedSince == self::gmtdate($this->_lmTime));
}
private static function gmtdate($ts) {
protected static function gmtdate($ts) {
return gmdate('D, d M Y H:i:s \G\M\T', $ts);
}
}

View File

@@ -104,7 +104,7 @@ class HTTP_ConditionalGet_Build {
file_put_contents($file, "{$this->lastModified}|{$nextCheck}");
}
private static function _scan($max, $path, $options)
protected static function _scan($max, $path, $options)
{
$d = dir($path);
while (false !== ($entry = $d->read())) {

View File

@@ -23,70 +23,134 @@
*/
class HTTP_Encoder {
/**
* Default compression level for zlib operations
*
* This level is used if encode() is not given a $compressionLevel
*
* @var int
*/
public static $compressionLevel = 6;
private static $clientEncodeMethod = null;
private $content = '';
private $headers = array();
private $encodeMethod = array('', '');
/**
* 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)
*
* @return null
*/
public function __construct($spec) {
if (isset($spec['content'])) {
$this->content = $spec['content'];
}
$this->headers['Content-Length'] = strlen($this->content);
$this->_content = $spec['content'];
$this->_headers['Content-Length'] = (string)strlen($this->_content);
if (isset($spec['type'])) {
$this->headers['Content-Type'] = $spec['type'];
$this->_headers['Content-Type'] = $spec['type'];
}
if (self::$clientEncodeMethod === null) {
self::$clientEncodeMethod = self::getAcceptedEncoding();
if (self::$_clientEncodeMethod === null) {
self::$_clientEncodeMethod = self::getAcceptedEncoding();
}
if (isset($spec['method'])
&& in_array($spec['method'], array('gzip', 'deflate', 'compress', '')))
{
$this->encodeMethod = array($spec['method'], $spec['method']);
$this->_encodeMethod = array($spec['method'], $spec['method']);
} else {
$this->encodeMethod = self::$clientEncodeMethod;
$this->_encodeMethod = self::$_clientEncodeMethod;
}
}
/**
* Get content in current form
*
* Call after encode() for encoded content.
*
* return string
*/
public function getContent() {
return $this->content;
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;
return $this->_headers;
}
/**
* Send the file and headers (encoded or not)
* 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()
*
* @return null
*/
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.
*
* @return null
*/
public function sendAll() {
$this->sendHeaders();
echo $this->content;
echo $this->_content;
}
/**
* Send just the headers
* 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.
*
* Note: this value is cached internally for the entire PHP execution
*
* @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 function sendHeaders() {
foreach ($this->headers as $name => $val) {
header($name . ': ' . $val);
}
}
// returns array(encoding, encoding to use in Content-Encoding header)
// eg. array('gzip', 'x-gzip')
public static function getAcceptedEncoding() {
if (self::$clientEncodeMethod !== null) {
return self::$clientEncodeMethod;
if (self::$_clientEncodeMethod !== null) {
return self::$_clientEncodeMethod;
}
if (! isset($_SERVER['HTTP_ACCEPT_ENCODING'])
|| self::isBuggyIe())
|| self::_isBuggyIe())
{
return array('', '');
}
@@ -104,37 +168,55 @@ class HTTP_Encoder {
}
/**
* If conditionsEncode the content
* @return bool success
* 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.
*
* If successful, the Content-Length header is updated, and Content-Encoding
* and Vary headers are 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 (null === $compressionLevel) {
$compressionLevel = self::$compressionLevel;
}
if ('' === $this->encodeMethod[0]
if ('' === $this->_encodeMethod[0]
|| ($compressionLevel == 0)
|| !extension_loaded('zlib'))
{
return false;
}
if ($this->encodeMethod[0] === 'gzip') {
$encoded = gzencode($this->content, $compressionLevel);
} elseif ($this->encodeMethod[0] === 'deflate') {
$encoded = gzdeflate($this->content, $compressionLevel);
if ($this->_encodeMethod[0] === 'gzip') {
$encoded = gzencode($this->_content, $compressionLevel);
} elseif ($this->_encodeMethod[0] === 'deflate') {
$encoded = gzdeflate($this->_content, $compressionLevel);
} else {
$encoded = gzcompress($this->content, $compressionLevel);
$encoded = gzcompress($this->_content, $compressionLevel);
}
if (false === $encoded) {
return false;
}
$this->headers['Content-Length'] = strlen($encoded);
$this->headers['Content-Encoding'] = $this->encodeMethod[1];
$this->headers['Vary'] = 'Accept-Encoding';
$this->content = $encoded;
$this->_headers['Content-Length'] = strlen($encoded);
$this->_headers['Content-Encoding'] = $this->_encodeMethod[1];
$this->_headers['Vary'] = 'Accept-Encoding';
$this->_content = $encoded;
return true;
}
private static function isBuggyIe()
protected static $_clientEncodeMethod = null;
protected $content = '';
protected $headers = array();
protected $encodeMethod = array('', '');
protected static function _isBuggyIe()
{
if (strstr($_SERVER['HTTP_USER_AGENT'], 'Opera')
|| !preg_match('/^Mozilla\/4\.0 \(compatible; MSIE ([0-9]\.[0-9])/i', $_SERVER['HTTP_USER_AGENT'], $m))