diff --git a/composer.json b/composer.json
index 701894f..3bfb17d 100644
--- a/composer.json
+++ b/composer.json
@@ -16,11 +16,21 @@
"issues": "http://code.google.com/p/minify/issues/list",
"wiki": "http://code.google.com/p/minify/w/list"
},
+ "autoload": {
+ "classmap": ["min/lib/"]
+ },
"require": {
"php": ">=5.2.1",
"ext-pcre": "*"
},
- "autoload": {
- "classmap": ["min/lib/"]
+ "require-dev": {
+ "leafo/lessphp": "~0.4.0",
+ "meenie/javascript-packer": "~1.1",
+ "tubalmartin/cssmin": "~2.4.8"
+ },
+ "suggest": {
+ "leafo/lessphp": "LESS support",
+ "meenie/javascript-packer": "Keep track of the Packer PHP port using Composer",
+ "tubalmartin/cssmin": "Support minify with CSSMin (YUI PHP port)"
}
}
diff --git a/min/builder/index.php b/min/builder/index.php
index 78a533b..35e10a9 100644
--- a/min/builder/index.php
+++ b/min/builder/index.php
@@ -220,22 +220,34 @@ by Minify. E.g. @import "/min/?g=css2";
<
$content
- ,'id' => __FILE__
- ,'lastModifiedTime' => max(
- // regenerate cache if any of these change
- filemtime(__FILE__)
- ,filemtime(dirname(__FILE__) . '/../config.php')
- ,filemtime(dirname(__FILE__) . '/../lib/Minify.php')
- )
- ,'minifyAll' => true
- ,'encodeOutput' => $encodeOutput
+$env = new Minify_Env();
+
+$sourceFactory = new Minify_Source_Factory($env, array(
+ 'uploaderHoursBehind' => $min_uploaderHoursBehind,
+));
+
+$controller = new Minify_Controller_Page($env, $sourceFactory);
+
+$server = new Minify($cache);
+
+$server->serve($controller, array(
+ 'content' => $content,
+ 'id' => __FILE__,
+ 'lastModifiedTime' => max(
+ // regenerate cache if any of these change
+ filemtime(__FILE__),
+ filemtime(dirname(__FILE__) . '/../config.php'),
+ filemtime(dirname(__FILE__) . '/../lib/Minify.php')
+ ),
+ 'minifyAll' => true,
+ 'encodeOutput' => $encodeOutput,
));
diff --git a/min/groupsConfig.php b/min/groupsConfig.php
index c900776..2131f05 100644
--- a/min/groupsConfig.php
+++ b/min/groupsConfig.php
@@ -12,6 +12,8 @@
**/
return array(
- // 'js' => array('//js/file1.js', '//js/file2.js'),
- // 'css' => array('//css/file1.css', '//css/file2.css'),
+// 'testJs' => array('//minify/min/quick-test.js'),
+// 'testCss' => array('//minify/min/quick-test.css'),
+// 'js' => array('//js/file1.js', '//js/file2.js'),
+// 'css' => array('//css/file1.css', '//css/file2.css'),
);
\ No newline at end of file
diff --git a/min/index.php b/min/index.php
index a49ad0b..f23d508 100644
--- a/min/index.php
+++ b/min/index.php
@@ -1,6 +1,6 @@
MINIFY_MIN_DIR . '/config.php',
'test' => MINIFY_MIN_DIR . '/config-test.php',
- 'groups' => MINIFY_MIN_DIR . '/groupsConfig.php'
+ 'groups' => MINIFY_MIN_DIR . '/groupsConfig.php',
);
// check for custom config paths
@@ -31,17 +31,29 @@ if (isset($_GET['test'])) {
require "$min_libPath/Minify/Loader.php";
Minify_Loader::register();
-Minify::$uploaderHoursBehind = $min_uploaderHoursBehind;
-Minify::setCache(
- isset($min_cachePath) ? $min_cachePath : ''
- ,$min_cacheFileLocking
-);
-
+// use an environment object to encapsulate all input
+$server = $_SERVER;
if ($min_documentRoot) {
- $_SERVER['DOCUMENT_ROOT'] = $min_documentRoot;
- Minify::$isDocRootSet = true;
+ $server['DOCUMENT_ROOT'] = $min_documentRoot;
+}
+$env = new Minify_Env(array(
+ 'server' => $server,
+));
+
+// setup cache
+if (!isset($min_cachePath)) {
+ $min_cachePath = '';
+}
+if (is_string($min_cachePath)) {
+ $cache = new Minify_Cache_File($min_cachePath, $min_cacheFileLocking);
+} else {
+ $cache = $min_cachePath;
}
+$server = new Minify($cache);
+
+// TODO probably should do this elsewhere...
+$min_serveOptions['minifierOptions']['text/css']['docRoot'] = $env->getDocRoot();
$min_serveOptions['minifierOptions']['text/css']['symlinks'] = $min_symlinks;
// auto-add targets to allowDirs
foreach ($min_symlinks as $uri => $target) {
@@ -49,38 +61,52 @@ foreach ($min_symlinks as $uri => $target) {
}
if ($min_allowDebugFlag) {
- $min_serveOptions['debug'] = Minify_DebugDetector::shouldDebugRequest($_COOKIE, $_GET, $_SERVER['REQUEST_URI']);
+ // TODO get rid of static stuff
+ $min_serveOptions['debug'] = Minify_DebugDetector::shouldDebugRequest($env);
}
if ($min_errorLogger) {
if (true === $min_errorLogger) {
$min_errorLogger = FirePHP::getInstance(true);
}
+ // TODO get rid of global state
Minify_Logger::setLogger($min_errorLogger);
}
// check for URI versioning
-if (preg_match('/&\\d/', $_SERVER['QUERY_STRING']) || isset($_GET['v'])) {
+if (null !== $env->get('v') || preg_match('/&\\d/', $env->server('QUERY_STRING'))) {
$min_serveOptions['maxAge'] = 31536000;
}
// need groups config?
-if (isset($_GET['g'])) {
+if (null !== $env->get('g')) {
// well need groups config
$min_serveOptions['minApp']['groups'] = (require $min_configPaths['groups']);
}
-// serve or redirect
-if (isset($_GET['f']) || isset($_GET['g'])) {
+if ($env->get('f') || null !== $env->get('g')) {
+ // serving!
if (! isset($min_serveController)) {
- $min_serveController = new Minify_Controller_MinApp();
+
+ $sourceFactoryOptions = array();
+
+ // translate legacy setting to option for source factory
+ if (isset($min_serveOptions['minApp']['noMinPattern'])) {
+ $sourceFactoryOptions['noMinPattern'] = $min_serveOptions['minApp']['noMinPattern'];
+ }
+ $sourceFactory = new Minify_Source_Factory($env, $sourceFactoryOptions, $cache);
+
+ $min_serveController = new Minify_Controller_MinApp($env, $sourceFactory);
}
- Minify::serve($min_serveController, $min_serveOptions);
-
-} elseif ($min_enableBuilder) {
- header('Location: builder/');
- exit;
-} else {
- header('Location: /');
+ $server->serve($min_serveController, $min_serveOptions);
exit;
}
+
+// not serving
+if ($min_enableBuilder) {
+ header('Location: builder/');
+ exit;
+}
+
+header('Location: /');
+exit;
diff --git a/min/lib/Minify.php b/min/lib/Minify.php
index e34fafe..89470ab 100644
--- a/min/lib/Minify.php
+++ b/min/lib/Minify.php
@@ -31,78 +31,96 @@ class Minify {
// 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
+ * Any Minify_Cache_* object or null (i.e. no server cache is used)
+ *
+ * @var Minify_CacheInterface
*/
- public static $isDocRootSet = false;
+ private $cache = null;
/**
- * 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.
+ * Active controller for current request
*
- * @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
+ * @var Minify_Controller_Base
*/
- public static function setCache($cache = '', $fileLocking = true)
- {
- if (is_string($cache)) {
- self::$_cache = new Minify_Cache_File($cache, $fileLocking);
- } else {
- self::$_cache = $cache;
- }
+ protected $controller = null;
+
+ /**
+ * @var Minify_SourceInterface[]
+ */
+ protected $sources;
+
+ /**
+ * @var string
+ */
+ protected $selectionId;
+
+ /**
+ * Options for current request
+ *
+ * @var array
+ */
+ protected $options = null;
+
+ /**
+ * @param Minify_CacheInterface $cache
+ */
+ public function __construct(Minify_CacheInterface $cache) {
+ $this->cache = $cache;
}
/**
- * Get Minify cache, if no Cache is defined, create Minify_Cache_Null
+ * Get default Minify options.
*
- * @return Minify_CacheInterface
+ * @return array options for Minify
*/
- public static function getCache()
+ public function getDefaultOptions()
{
- if (!self::$_cache) {
- self::$_cache = new Minify_Cache_Null();
- }
- return self::$_cache;
+ return array(
+ 'isPublic' => true,
+ 'encodeOutput' => function_exists('gzdeflate'),
+ 'encodeMethod' => null, // determine later
+ 'encodeLevel' => 9,
+
+ 'minifiers' => array(
+ Minify::TYPE_JS => array('JSMin', 'minify'),
+ Minify::TYPE_CSS => array('Minify_CSS', 'minify'),
+ Minify::TYPE_HTML => array('Minify_HTML', 'minify'),
+ ),
+ '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,
+
+ /**
+ * 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.
+ */
+ 'importWarning' => "/* See http://code.google.com/p/minify/wiki/CommonProblems#@imports_can_appear_in_invalid_locations_in_combined_CSS_files */\n"
+ );
}
/**
* Serve a request for a minified file.
*
- * Here are the available options and defaults in the base controller:
+ * Here are the available options and defaults:
*
* 'isPublic' : send "public" instead of "private" in Cache-Control
* headers, allowing shared caches to cache the output. (default true)
@@ -160,13 +178,16 @@ class Minify {
* 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.
+ *
+ * 'importWarning' : 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. To disable this, set this option to empty string.
*
- * Any controller options are documented in that controller's setupSources() method.
+ * Any controller options are documented in that controller's createConfiguration() method.
*
- * @param mixed $controller instance of subclass of Minify_Controller_Base or string
- * name of controller. E.g. 'Files'
+ * @param Minify_ControllerInterface $controller instance of subclass of Minify_Controller_Base
*
- * @param array $options controller/serve options
+ * @param array $options controller/serve options
*
* @return null|array if the 'quiet' option is set to true, an array
* with keys "success" (bool), "statusCode" (int), "content" (string), and
@@ -174,89 +195,82 @@ class Minify {
*
* @throws Exception
*/
- public static function serve($controller, $options = array())
+ public function serve(Minify_ControllerInterface $controller, $options = array())
{
- if (! self::$isDocRootSet && 0 === stripos(PHP_OS, 'win')) {
- self::setDocRoot();
- }
+ $options = array_merge($this->getDefaultOptions(), $options);
+
+ $config = $controller->createConfiguration($options);
+
+ $this->sources = $config->getSources();
+ $this->selectionId = $config->getSelectionId();
+ $this->options = $this->analyzeSources($config->getOptions());
- if (is_string($controller)) {
- // make $controller into object
- $class = 'Minify_Controller_' . $controller;
- $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) {
+ if (!$this->sources) {
// invalid request!
- if (! self::$_options['quiet']) {
- self::_errorExit(self::$_options['badRequestHeader'], self::URL_DEBUG);
+ if (! $this->options['quiet']) {
+ $this->errorExit($this->options['badRequestHeader'], self::URL_DEBUG);
} else {
- list(,$statusCode) = explode(' ', self::$_options['badRequestHeader']);
+ list(,$statusCode) = explode(' ', $this->options['badRequestHeader']);
return array(
- 'success' => false
- ,'statusCode' => (int)$statusCode
- ,'content' => ''
- ,'headers' => array()
+ 'success' => false,
+ 'statusCode' => (int)$statusCode,
+ 'content' => '',
+ 'headers' => array(),
);
}
}
- self::$_controller = $controller;
+ $this->controller = $controller;
- if (self::$_options['debug']) {
- self::_setupDebug($controller->sources);
- self::$_options['maxAge'] = 0;
+ if ($this->options['debug']) {
+ $this->setupDebug();
+ $this->options['maxAge'] = 0;
}
// determine encoding
- if (self::$_options['encodeOutput']) {
+ if ($this->options['encodeOutput']) {
$sendVary = true;
- if (self::$_options['encodeMethod'] !== null) {
+ if ($this->options['encodeMethod'] !== null) {
// controller specifically requested this
- $contentEncoding = self::$_options['encodeMethod'];
+ $contentEncoding = $this->options['encodeMethod'];
} else {
// sniff request header
// 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);
+ list($this->options['encodeMethod'], $contentEncoding) = HTTP_Encoder::getAcceptedEncoding(false, false);
$sendVary = ! HTTP_Encoder::isBuggyIe();
}
} else {
- self::$_options['encodeMethod'] = ''; // identity (no encoding)
+ $this->options['encodeMethod'] = ''; // identity (no encoding)
}
// check client cache
$cgOptions = array(
- 'lastModifiedTime' => self::$_options['lastModifiedTime']
- ,'isPublic' => self::$_options['isPublic']
- ,'encoding' => self::$_options['encodeMethod']
+ 'lastModifiedTime' => $this->options['lastModifiedTime'],
+ 'isPublic' => $this->options['isPublic'],
+ 'encoding' => $this->options['encodeMethod'],
);
- if (self::$_options['maxAge'] > 0) {
- $cgOptions['maxAge'] = self::$_options['maxAge'];
- } elseif (self::$_options['debug']) {
+
+ if ($this->options['maxAge'] > 0) {
+ $cgOptions['maxAge'] = $this->options['maxAge'];
+ } elseif ($this->options['debug']) {
$cgOptions['invalidate'] = true;
}
+
$cg = new HTTP_ConditionalGet($cgOptions);
if ($cg->cacheIsValid) {
// client's cache is valid
- if (! self::$_options['quiet']) {
+ if (! $this->options['quiet']) {
$cg->sendHeaders();
return;
} else {
return array(
- 'success' => true
- ,'statusCode' => 304
- ,'content' => ''
- ,'headers' => $cg->getHeaders()
+ 'success' => true,
+ 'statusCode' => 304,
+ 'content' => '',
+ 'headers' => $cg->getHeaders(),
);
}
} else {
@@ -265,96 +279,96 @@ class Minify {
unset($cg);
}
- if (self::$_options['contentType'] === self::TYPE_CSS
- && self::$_options['rewriteCssUris']) {
- foreach($controller->sources as $key => $source) {
- $source->setupUriRewrites();
- }
+ if ($this->options['contentType'] === self::TYPE_CSS && $this->options['rewriteCssUris']) {
+ $this->setupUriRewrites();
}
// check server cache
- if (null !== self::$_cache && ! self::$_options['debug']) {
+ if (! $this->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;
+ $cacheId = $this->_getCacheId();
+ $fullCacheId = ($this->options['encodeMethod']) ? $cacheId . '.gz' : $cacheId;
+
// check cache for valid entry
- $cacheIsReady = self::$_cache->isValid($fullCacheId, self::$_options['lastModifiedTime']);
+ $cacheIsReady = $this->cache->isValid($fullCacheId, $this->options['lastModifiedTime']);
if ($cacheIsReady) {
- $cacheContentLength = self::$_cache->getSize($fullCacheId);
+ $cacheContentLength = $this->cache->getSize($fullCacheId);
} else {
// generate & cache content
try {
- $content = self::_combineMinify();
+ $content = $this->combineMinify();
} catch (Exception $e) {
- self::$_controller->log($e->getMessage());
- if (! self::$_options['quiet']) {
- self::_errorExit(self::$_options['errorHeader'], self::URL_DEBUG);
+ $this->controller->log($e->getMessage());
+ if (! $this->options['quiet']) {
+ $this->errorExit($this->options['errorHeader'], self::URL_DEBUG);
}
throw $e;
}
- self::$_cache->store($cacheId, $content);
- if (function_exists('gzencode') && self::$_options['encodeMethod']) {
- self::$_cache->store($cacheId . '.gz', gzencode($content, self::$_options['encodeLevel']));
+ $this->cache->store($cacheId, $content);
+ if (function_exists('gzencode') && $this->options['encodeMethod']) {
+ $this->cache->store($cacheId . '.gz', gzencode($content, $this->options['encodeLevel']));
}
}
} else {
// no cache
$cacheIsReady = false;
try {
- $content = self::_combineMinify();
+ $content = $this->combineMinify();
} catch (Exception $e) {
- self::$_controller->log($e->getMessage());
- if (! self::$_options['quiet']) {
- self::_errorExit(self::$_options['errorHeader'], self::URL_DEBUG);
+ $this->controller->log($e->getMessage());
+ if (! $this->options['quiet']) {
+ $this->errorExit($this->options['errorHeader'], self::URL_DEBUG);
}
throw $e;
}
}
- if (! $cacheIsReady && self::$_options['encodeMethod']) {
+ if (! $cacheIsReady && $this->options['encodeMethod']) {
// still need to encode
- $content = gzencode($content, self::$_options['encodeLevel']);
+ $content = gzencode($content, $this->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'] !== '') {
+ if ($cacheIsReady) {
+ $headers['Content-Length'] = $cacheContentLength;
+ } else {
+ if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) {
+ $headers['Content-Length'] = mb_strlen($content, '8bit');
+ } else {
+ $headers['Content-Length'] = strlen($content);
+ }
+ }
+
+ $headers['Content-Type'] = $this->options['contentType'];
+ if ($this->options['contentTypeCharset']) {
+ $headers['Content-Type'] .= '; charset=' . $this->options['contentTypeCharset'];
+ }
+
+ if ($this->options['encodeMethod'] !== '') {
$headers['Content-Encoding'] = $contentEncoding;
}
- if (self::$_options['encodeOutput'] && $sendVary) {
+ if ($this->options['encodeOutput'] && $sendVary) {
$headers['Vary'] = 'Accept-Encoding';
}
- if (! self::$_options['quiet']) {
+ if (! $this->options['quiet']) {
// output headers & content
foreach ($headers as $name => $val) {
header($name . ': ' . $val);
}
if ($cacheIsReady) {
- self::$_cache->display($fullCacheId);
+ $this->cache->display($fullCacheId);
} else {
echo $content;
}
} else {
return array(
- 'success' => true
- ,'statusCode' => 200
- ,'content' => $cacheIsReady
- ? self::$_cache->fetch($fullCacheId)
- : $content
- ,'headers' => $headers
+ 'success' => true,
+ 'statusCode' => 200,
+ 'content' => $cacheIsReady ? $this->cache->fetch($fullCacheId) : $content,
+ 'headers' => $headers,
);
}
}
@@ -371,68 +385,34 @@ class Minify {
*
* @return string
*/
- public static function combine($sources, $options = array())
+ public function combine($sources, $options = array())
{
- $cache = self::$_cache;
- self::$_cache = null;
+ throw new BadMethodCallException(__METHOD__ . ' needs to be rewritten/replaced');
+
+ $cache = $this->cache;
+ $this->cache = new Minify_Cache_Null();
+
$options = array_merge(array(
- 'files' => (array)$sources
- ,'quiet' => true
- ,'encodeMethod' => ''
- ,'lastModifiedTime' => 0
+ 'files' => (array)$sources,
+ 'quiet' => true,
+ 'encodeMethod' => '',
+ 'lastModifiedTime' => 0,
), $options);
- $out = self::serve('Files', $options);
- self::$_cache = $cache;
+
+ $sourceFactory = new Minify_Source_Factory($this->env);
+ $controller = new Minify_Controller_Files($this->env, $sourceFactory);
+ $out = $this->serve($controller, $options);
+
+ $this->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_CacheInterface
- */
- 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)
+ protected function errorExit($header, $url)
{
$url = htmlspecialchars($url, ENT_QUOTES);
list(,$h1) = explode(' ', $header, 2);
@@ -447,17 +427,34 @@ class Minify {
}
/**
- * Set up sources to use Minify_Lines
- *
- * @param Minify_Source[] $sources Minify_Source instances
+ * Setup CSS sources for URI rewriting
*/
- protected static function _setupDebug($sources)
+ protected function setupUriRewrites()
{
- foreach ($sources as $source) {
+ foreach($this->sources as $key => $source) {
+ $file = $source->getFilePath();
+ $minifyOptions = $source->getMinifierOptions();
+
+ if ($file
+ && !isset($minifyOptions['currentDir'])
+ && !isset($minifyOptions['prependRelativePath'])
+ ) {
+ $minifyOptions['currentDir'] = dirname($file);
+ $source->setMinifierOptions($minifyOptions);
+ }
+ }
+ }
+
+ /**
+ * Set up sources to use Minify_Lines
+ */
+ protected function setupDebug()
+ {
+ foreach ($this->sources as $source) {
$source->setMinifier(array('Minify_Lines', 'minify'));
$id = $source->getId();
$source->setMinifierOptions(array(
- 'id' => (is_file($id) ? basename($id) : $id)
+ 'id' => (is_file($id) ? basename($id) : $id),
));
}
}
@@ -469,31 +466,35 @@ class Minify {
*
* @throws Exception
*/
- protected static function _combineMinify()
+ protected function combineMinify()
{
- $type = self::$_options['contentType']; // ease readability
+ $type = $this->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;"
- : '';
+ $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 (isset($this->options['minifierOptions'][$type])) {
+ $defaultOptions = $this->options['minifierOptions'][$type];
+ } else {
+ $defaultOptions = 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;
+ if (isset($this->options['minifiers'][$type])) {
+ $defaultMinifier = $this->options['minifiers'][$type];
+ } else {
+ $defaultMinifier = false;
+ }
// process groups of sources with identical minifiers/options
$content = array();
$i = 0;
- $l = count(self::$_controller->sources);
+ $l = count($this->sources);
$groupToProcessTogether = array();
$lastMinifier = null;
$lastOptions = null;
@@ -501,7 +502,7 @@ class Minify {
// get next source
$source = null;
if ($i < $l) {
- $source = self::$_controller->sources[$i];
+ $source = $this->sources[$i];
/* @var Minify_Source $source */
$sourceContent = $source->getContent();
@@ -546,15 +547,15 @@ class Minify {
$content = implode($implodeSeparator, $content);
if ($type === self::TYPE_CSS && false !== strpos($content, '@import')) {
- $content = self::_handleCssImports($content);
+ $content = $this->handleCssImports($content);
}
// do any post-processing (esp. for editing build URIs)
- if (self::$_options['postprocessorRequire']) {
- require_once self::$_options['postprocessorRequire'];
+ if ($this->options['postprocessorRequire']) {
+ require_once $this->options['postprocessorRequire'];
}
- if (self::$_options['postprocessor']) {
- $content = call_user_func(self::$_options['postprocessor'], $content, $type);
+ if ($this->options['postprocessor']) {
+ $content = call_user_func($this->options['postprocessor'], $content, $type);
}
return $content;
}
@@ -568,18 +569,18 @@ class Minify {
*
* @return string
*/
- protected static function _getCacheId($prefix = 'minify')
+ protected function _getCacheId($prefix = 'minify')
{
- $name = preg_replace('/[^a-zA-Z0-9\\.=_,]/', '', self::$_controller->selectionId);
+ $name = preg_replace('/[^a-zA-Z0-9\\.=_,]/', '', $this->selectionId);
$name = preg_replace('/\\.+/', '.', $name);
$name = substr($name, 0, 100 - 34 - strlen($prefix));
$md5 = md5(serialize(array(
- Minify_SourceSet::getDigest(self::$_controller->sources)
- ,self::$_options['minifiers']
- ,self::$_options['minifierOptions']
- ,self::$_options['postprocessor']
- ,self::$_options['bubbleCssImports']
- ,self::VERSION
+ Minify_SourceSet::getDigest($this->sources),
+ $this->options['minifiers'],
+ $this->options['minifierOptions'],
+ $this->options['postprocessor'],
+ $this->options['bubbleCssImports'],
+ Minify::VERSION,
)));
return "{$prefix}_{$name}_{$md5}";
}
@@ -591,25 +592,74 @@ class Minify {
*
* @return string
*/
- protected static function _handleCssImports($css)
+ protected function handleCssImports($css)
{
- if (self::$_options['bubbleCssImports']) {
+ if ($this->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;
+ }
+
+ if ('' === $this->options['importWarning']) {
+ return $css;
+ }
+
+ // 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 = $this->options['importWarning'] . $css;
}
return $css;
}
+
+ /**
+ * 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
+ */
+ protected function analyzeSources($options = array())
+ {
+ if (!$this->sources) {
+ return $options;
+ }
+
+ $type = null;
+ foreach ($this->sources as $source) {
+ if ($type === null) {
+ $type = $source->getContentType();
+ } elseif ($source->getContentType() !== $type) {
+
+ // TODO better logging
+ Minify_Logger::log('ContentType mismatch');
+
+ $this->sources = array();
+ return $options;
+ }
+ }
+ if (null === $type) {
+ $type = 'text/plain';
+ }
+ $options['contentType'] = $type;
+
+ // 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->getLastModified(), $max);
+ }
+ $options['lastModifiedTime'] = $max;
+ }
+
+ return $options;
+ }
}
diff --git a/min/lib/Minify/Cache/Null.php b/min/lib/Minify/Cache/Null.php
index 1e592c2..38c77c7 100644
--- a/min/lib/Minify/Cache/Null.php
+++ b/min/lib/Minify/Cache/Null.php
@@ -3,6 +3,9 @@
/**
* Class Minify_Cache_Null
*
+ * If this is used, Minify will not use a cache and, for each 200 response, will
+ * need to recombine files, minify and encode the output.
+ *
* @package Minify
*/
class Minify_Cache_Null implements Minify_CacheInterface {
diff --git a/min/lib/Minify/Controller/Base.php b/min/lib/Minify/Controller/Base.php
index 2d8bd9c..b601d77 100644
--- a/min/lib/Minify/Controller/Base.php
+++ b/min/lib/Minify/Controller/Base.php
@@ -7,207 +7,42 @@
/**
* 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.
+ * The controller class validates a request and uses it to create a configuration for Minify::serve().
*
* @package Minify
* @author Stephen Clay
*/
-abstract class Minify_Controller_Base {
-
+abstract class Minify_Controller_Base implements Minify_ControllerInterface {
+
/**
- * 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.
+ * @var Minify_Env
+ */
+ protected $env;
+
+ /**
+ * @var Minify_Source_Factory
+ */
+ protected $sourceFactory;
+
+ /**
+ * @param Minify_Env $env
+ * @param Minify_Source_Factory $sourceFactory
+ */
+ public function __construct(Minify_Env $env, Minify_Source_Factory $sourceFactory)
+ {
+ $this->env = $env;
+ $this->sourceFactory = $sourceFactory;
+ }
+
+ /**
+ * Create controller sources and options for Minify::serve()
*
* @param array $options controller and Minify options
*
- * @return array $options Minify::serve options
+ * @return Minify_ServeConfiguration
*/
- abstract public function setupSources($options);
+ abstract public function createConfiguration(array $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;
- }
-
- /**
- * 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 Minify_SourceInterface[]
- */
- 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_SourceSet::getContentType($this->sources);
- }
- // last modified is needed for caching, even if setExpires is set
- if (! isset($options['lastModifiedTime'])) {
- $max = 0;
- /** @var Minify_Source $source */
- foreach ($this->sources as $source) {
- $max = max($source->getLastModified(), $max);
- }
- $options['lastModifiedTime'] = $max;
- }
- }
- return $options;
- }
-
/**
* Send message to the Minify logger
*
@@ -215,7 +50,8 @@ abstract class Minify_Controller_Base {
*
* @return null
*/
- public function log($msg) {
+ public function log($msg)
+ {
Minify_Logger::log($msg);
}
}
diff --git a/min/lib/Minify/Controller/Files.php b/min/lib/Minify/Controller/Files.php
index f084cd0..29cf712 100644
--- a/min/lib/Minify/Controller/Files.php
+++ b/min/lib/Minify/Controller/Files.php
@@ -17,9 +17,6 @@
* )
* ));
*
- *
- * As a shortcut, the controller will replace "//" at the beginning
- * of a filename with $_SERVER['DOCUMENT_ROOT'] . '/'.
*
* @package Minify
* @author Stephen Clay
@@ -30,13 +27,13 @@ class Minify_Controller_Files extends Minify_Controller_Base {
* Set up file sources
*
* @param array $options controller and Minify options
- * @return array Minify options
+ * @return Minify_ServeConfiguration
*
* Controller options:
*
* 'files': (required) array of complete file paths, or a single path
*/
- public function setupSources($options) {
+ public function createConfiguration(array $options) {
// strip controller options
$files = $options['files'];
@@ -50,27 +47,20 @@ class Minify_Controller_Files extends Minify_Controller_Base {
$sources = array();
foreach ($files as $file) {
- if ($file instanceof Minify_Source) {
+ if ($file instanceof Minify_SourceInterface) {
$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;
+ try {
+ $sources[] = $this->sourceFactory->makeSource(array(
+ 'filepath' => $file,
+ ));
+ } catch (Minify_Source_FactoryException $e) {
+ $this->log($e->getMessage());
+ return new Minify_ServeConfiguration($options);
}
}
- if ($sources) {
- $this->sources = $sources;
- }
- return $options;
+ return new Minify_ServeConfiguration($options, $sources);
}
}
diff --git a/min/lib/Minify/Controller/Groups.php b/min/lib/Minify/Controller/Groups.php
index c4c25db..9fec54c 100644
--- a/min/lib/Minify/Controller/Groups.php
+++ b/min/lib/Minify/Controller/Groups.php
@@ -20,13 +20,10 @@
* 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
*/
-class Minify_Controller_Groups extends Minify_Controller_Base {
+class Minify_Controller_Groups extends Minify_Controller_Files {
/**
* Set up groups of files as sources
@@ -38,54 +35,37 @@ class Minify_Controller_Groups extends Minify_Controller_Base {
*
* @return array Minify options
*/
- public function setupSources($options) {
+ public function createConfiguration(array $options) {
// strip controller options
$groups = $options['groups'];
unset($options['groups']);
+
+ $server = $this->env->server();
// 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)
+ $pathInfo = 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])) {
+ if (false === $pathInfo || ! isset($groups[$pathInfo])) {
// no PATH_INFO or not a valid group
- $this->log("Missing PATH_INFO or no group set for \"$pi\"");
- return $options;
+ $this->log("Missing PATH_INFO or no group set for \"$pathInfo\"");
+ return new Minify_ServeConfiguration($options);
}
- $sources = array();
-
- $files = $groups[$pi];
+
+ $files = $groups[$pathInfo];
// 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;
+
+ $options['files'] = $files;
+
+ return parent::createConfiguration($options);
}
}
diff --git a/min/lib/Minify/Controller/MinApp.php b/min/lib/Minify/Controller/MinApp.php
index 23d5ac9..e2f4b72 100644
--- a/min/lib/Minify/Controller/MinApp.php
+++ b/min/lib/Minify/Controller/MinApp.php
@@ -11,7 +11,7 @@
* @author Stephen Clay
*/
class Minify_Controller_MinApp extends Minify_Controller_Base {
-
+
/**
* Set up groups of files as sources
*
@@ -19,43 +19,44 @@ class Minify_Controller_MinApp extends Minify_Controller_Base {
*
* @return array Minify options
*/
- public function setupSources($options) {
+ public function createConfiguration(array $options) {
// PHP insecure by default: realpath() and other FS functions can't handle null bytes.
+ $get = $this->env->get();
foreach (array('g', 'b', 'f') as $key) {
- if (isset($_GET[$key])) {
- $_GET[$key] = str_replace("\x00", '', (string)$_GET[$key]);
+ if (isset($get[$key])) {
+ $get[$key] = str_replace("\x00", '', (string)$get[$key]);
}
}
// filter controller options
- $cOptions = array_merge(
+ $localOptions = array_merge(
array(
- 'allowDirs' => '//'
- ,'groupsOnly' => false
- ,'groups' => array()
- ,'noMinPattern' => '@[-\\.]min\\.(?:js|css)$@i' // matched against basename
+ 'groupsOnly' => false,
+ 'groups' => array(),
)
,(isset($options['minApp']) ? $options['minApp'] : array())
);
unset($options['minApp']);
+
$sources = array();
- $this->selectionId = '';
+ $selectionId = '';
$firstMissingResource = null;
- if (isset($_GET['g'])) {
+
+ if (isset($get['g'])) {
// add group(s)
- $this->selectionId .= 'g=' . $_GET['g'];
- $keys = explode(',', $_GET['g']);
+ $selectionId .= 'g=' . $get['g'];
+ $keys = explode(',', $get['g']);
if ($keys != array_unique($keys)) {
$this->log("Duplicate group key found.");
- return $options;
+ return new Minify_ServeConfiguration($options);
}
- $keys = explode(',', $_GET['g']);
+ $keys = explode(',', $get['g']);
foreach ($keys as $key) {
- if (! isset($cOptions['groups'][$key])) {
+ if (! isset($localOptions['groups'][$key])) {
$this->log("A group configuration for \"{$key}\" was not found");
- return $options;
+ return new Minify_ServeConfiguration($options);
}
- $files = $cOptions['groups'][$key];
+ $files = $localOptions['groups'][$key];
// if $files is a single object, casting will break it
if (is_object($files)) {
$files = array($files);
@@ -63,182 +64,110 @@ class Minify_Controller_MinApp extends Minify_Controller_Base {
$files = (array)$files;
}
foreach ($files as $file) {
- if ($file instanceof Minify_Source) {
+ if ($file instanceof Minify_SourceInterface) {
$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)");
+ try {
+ $source = $this->sourceFactory->makeSource(array(
+ 'filepath' => $file,
+ ));
+ $sources[] = $source;
+ } catch (Minify_Source_FactoryException $e) {
+ $this->log($e->getMessage());
if (null === $firstMissingResource) {
$firstMissingResource = basename($file);
continue;
} else {
$secondMissingResource = basename($file);
$this->log("More than one file was missing: '$firstMissingResource', '$secondMissingResource'");
- return $options;
+ return new Minify_ServeConfiguration($options);
}
}
}
- if ($sources) {
- try {
- $this->checkType($sources[0]);
- } catch (Exception $e) {
- $this->log($e->getMessage());
- return $options;
- }
- }
}
}
- if (! $cOptions['groupsOnly'] && isset($_GET['f'])) {
+ if (! $localOptions['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)
+ ! preg_match('/^[^,]+\\.(css|less|js)(?:,[^,]+\\.\\1)*$/', $get['f'], $m)
// no "//"
- || strpos($_GET['f'], '//') !== false
+ || strpos($get['f'], '//') !== false
// no "\"
- || strpos($_GET['f'], '\\') !== false
+ || strpos($get['f'], '\\') !== false
) {
$this->log("GET param 'f' was invalid");
- return $options;
+ return new Minify_ServeConfiguration($options);
}
$ext = ".{$m[1]}";
- try {
- $this->checkType($m[1]);
- } catch (Exception $e) {
- $this->log($e->getMessage());
- return $options;
- }
- $files = explode(',', $_GET['f']);
+ $files = explode(',', $get['f']);
if ($files != array_unique($files)) {
$this->log("Duplicate files were specified");
- return $options;
+ return new Minify_ServeConfiguration($options);
}
- if (isset($_GET['b'])) {
+ if (isset($get['b'])) {
// check for validity
- if (preg_match('@^[^/]+(?:/[^/]+)*$@', $_GET['b'])
- && false === strpos($_GET['b'], '..')
- && $_GET['b'] !== '.') {
+ if (preg_match('@^[^/]+(?:/[^/]+)*$@', $get['b'])
+ && false === strpos($get['b'], '..')
+ && $get['b'] !== '.') {
// valid base
- $base = "/{$_GET['b']}/";
+ $base = "/{$get['b']}/";
} else {
$this->log("GET param 'b' was invalid");
- return $options;
+ return new Minify_ServeConfiguration($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)");
+ $path = $this->env->getDocRoot() . $uri;
+
+ try {
+ $source = $this->sourceFactory->makeSource(array(
+ 'filepath' => $path,
+ ));
+ $sources[] = $source;
+ $basenames[] = basename($path, $ext);
+ } catch (Minify_Source_FactoryException $e) {
+ $this->log($e->getMessage());
if (null === $firstMissingResource) {
$firstMissingResource = $uri;
continue;
} else {
$secondMissingResource = $uri;
$this->log("More than one file was missing: '$firstMissingResource', '$secondMissingResource`'");
- return $options;
+ return new Minify_ServeConfiguration($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=';
+ if ($selectionId) {
+ $selectionId .= '_f=';
}
- $this->selectionId .= implode(',', $basenames) . $ext;
+ $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 {
+
+ if (!$sources) {
$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))) {
- if (preg_match('~\.css$~i', $file)) {
- $spec['minifyOptions']['compress'] = false;
- } else {
- $spec['minifier'] = '';
- }
- }
- return new Minify_Source($spec);
- }
-
- protected $_type = null;
-
- /**
- * Make sure that only source files of a single type are registered
- *
- * @param Minify_SourceInterface|string $sourceOrExt
- *
- * @throws Exception
- */
- public function checkType($sourceOrExt)
- {
- if ($sourceOrExt instanceof Minify_SourceInterface) {
- $type = $sourceOrExt->getContentType();
- if (!$type) {
- return;
- }
- } else {
- if ($sourceOrExt === 'js') {
- $type = Minify::TYPE_JS;
- } elseif ($sourceOrExt === 'css') {
- $type = Minify::TYPE_CSS;
- } else {
- $type = "text/$sourceOrExt";
- }
+ return new Minify_ServeConfiguration($options);
}
- if ($this->_type === null) {
- $this->_type = $type;
- } elseif ($this->_type !== $type) {
- throw new Exception('Content-Type mismatch');
+ 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' => ''
+ )));
}
+
+ return new Minify_ServeConfiguration($options, $sources, $selectionId);
}
}
diff --git a/min/lib/Minify/Controller/Page.php b/min/lib/Minify/Controller/Page.php
index 1095fb4..96cd552 100644
--- a/min/lib/Minify/Controller/Page.php
+++ b/min/lib/Minify/Controller/Page.php
@@ -29,11 +29,9 @@ class Minify_Controller_Page extends Minify_Controller_Base {
* 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.
+ * minified? (default false)
*/
- public function setupSources($options) {
+ public function createConfiguration(array $options) {
if (isset($options['file'])) {
$sourceSpec = array(
'filepath' => $options['file']
@@ -49,7 +47,7 @@ class Minify_Controller_Page extends Minify_Controller_Base {
unset($options['content'], $options['id']);
}
// something like "builder,index.php" or "directory,file.html"
- $this->selectionId = strtr(substr($f, 1 + strlen(dirname(dirname($f)))), '/\\', ',,');
+ $selectionId = strtr(substr($f, 1 + strlen(dirname(dirname($f)))), '/\\', ',,');
if (isset($options['minifyAll'])) {
// this will be the 2nd argument passed to Minify_HTML::minify()
@@ -59,10 +57,11 @@ class Minify_Controller_Page extends Minify_Controller_Base {
);
unset($options['minifyAll']);
}
- $this->sources[] = new Minify_Source($sourceSpec);
-
- $options['contentType'] = Minify::TYPE_HTML;
- return $options;
+
+ $sourceSpec['contentType'] = Minify::TYPE_HTML;
+ $sources[] = new Minify_Source($sourceSpec);
+
+ return new Minify_ServeConfiguration($options, $sources, $selectionId);
}
}
diff --git a/min/lib/Minify/Controller/Version1.php b/min/lib/Minify/Controller/Version1.php
deleted file mode 100644
index 91fcf61..0000000
--- a/min/lib/Minify/Controller/Version1.php
+++ /dev/null
@@ -1,119 +0,0 @@
-
- * Minify::serve('Version1');
- *
- *
- * @package Minify
- * @author Stephen Clay
- */
-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) {
- // PHP insecure by default: realpath() and other FS functions can't handle null bytes.
- if (isset($_GET['files'])) {
- $_GET['files'] = str_replace("\x00", '', (string)$_GET['files']);
- }
-
- 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;
- }
-
- $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'];
-
- $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);
- }
- }
- }
-}
-
diff --git a/min/lib/Minify/ControllerInterface.php b/min/lib/Minify/ControllerInterface.php
new file mode 100644
index 0000000..3ef1684
--- /dev/null
+++ b/min/lib/Minify/ControllerInterface.php
@@ -0,0 +1,13 @@
+
*/
class Minify_DebugDetector {
- public static function shouldDebugRequest($cookie, $get, $requestUri)
+ public static function shouldDebugRequest(Minify_Env $env)
{
- if (isset($get['debug'])) {
+ if ($env->get('debug')) {
return true;
}
- if (! empty($cookie['minifyDebug'])) {
- foreach (preg_split('/\\s+/', $cookie['minifyDebug']) as $debugUri) {
+ $cookieValue = $env->cookie('minifyDebug');
+ if ($cookieValue) {
+ foreach (preg_split('/\\s+/', $cookieValue) as $debugUri) {
$pattern = '@' . preg_quote($debugUri, '@') . '@i';
$pattern = str_replace(array('\\*', '\\?'), array('.*', '.'), $pattern);
- if (preg_match($pattern, $requestUri)) {
+ if (preg_match($pattern, $env->getRequestUri())) {
return true;
}
}
diff --git a/min/lib/Minify/Env.php b/min/lib/Minify/Env.php
new file mode 100644
index 0000000..e9a931e
--- /dev/null
+++ b/min/lib/Minify/Env.php
@@ -0,0 +1,90 @@
+server['DOCUMENT_ROOT'];
+ }
+
+ /**
+ * @return null
+ */
+ public function getRequestUri()
+ {
+ return $this->server['REQUEST_URI'];
+ }
+
+ public function __construct($options = array())
+ {
+ $options = array_merge(array(
+ 'server' => $_SERVER,
+ 'get' => $_GET,
+ 'cookie' => $_COOKIE,
+ ), $options);
+
+ $this->server = $options['server'];
+ if (empty($this->server['DOCUMENT_ROOT'])) {
+ $this->server['DOCUMENT_ROOT'] = $this->computeDocRoot($options['server']);
+ }
+ $this->get = $options['get'];
+ $this->cookie = $options['cookie'];
+ }
+
+ public function server($key = null)
+ {
+ if (null === $key) {
+ return $this->server;
+ }
+ return isset($this->server[$key])
+ ? $this->server[$key]
+ : null;
+ }
+
+ public function cookie($key = null)
+ {
+ if (null === $key) {
+ return $this->cookie;
+ }
+ return isset($this->cookie[$key])
+ ? $this->cookie[$key]
+ : null;
+ }
+
+ public function get($key = null)
+ {
+ if (null === $key) {
+ return $this->get;
+ }
+ return isset($this->get[$key])
+ ? $this->get[$key]
+ : null;
+ }
+
+ protected $server = null;
+ protected $get = null;
+ protected $cookie = null;
+
+ /**
+ * Compute $_SERVER['DOCUMENT_ROOT'] for IIS using SCRIPT_FILENAME and SCRIPT_NAME.
+ *
+ * @param array $server
+ * @return string
+ */
+ protected function computeDocRoot(array $server)
+ {
+ if (empty($server['SERVER_SOFTWARE'])
+ || 0 !== strpos($server['SERVER_SOFTWARE'], 'Microsoft-IIS/')) {
+ throw new InvalidArgumentException('DOCUMENT_ROOT is not provided and could not be computed');
+ }
+ $docRoot = substr(
+ $server['SCRIPT_FILENAME']
+ ,0
+ ,strlen($server['SCRIPT_FILENAME']) - strlen($server['SCRIPT_NAME'])
+ );
+ return rtrim($docRoot, '\\');
+ }
+}
diff --git a/min/lib/Minify/LessCssSource.php b/min/lib/Minify/LessCssSource.php
new file mode 100644
index 0000000..e887706
--- /dev/null
+++ b/min/lib/Minify/LessCssSource.php
@@ -0,0 +1,107 @@
+cache = $cache;
+ }
+
+ /**
+ * Get last modified of all parsed files
+ *
+ * @return int
+ */
+ public function getLastModified() {
+ $cache = $this->getCache();
+
+ $lastModified = 0;
+ foreach ($cache['files'] as $mtime) {
+ $lastModified = max($lastModified, $mtime);
+
+ }
+ return $lastModified;
+ }
+
+ /**
+ * Get content
+ *
+ * @return string
+ */
+ public function getContent() {
+ $cache = $this->getCache();
+
+ return $cache['compiled'];
+ }
+
+ /**
+ * Get lessphp cache object
+ *
+ * @return array
+ */
+ private function getCache() {
+ // cache for single run
+ // so that getLastModified and getContent in single request do not add additional cache roundtrips (i.e memcache)
+ if (isset($this->parsed)) {
+ return $this->parsed;
+ }
+
+ // check from cache first
+ $cache = null;
+ $cacheId = $this->getCacheId();
+ if ($this->cache->isValid($cacheId, 0)) {
+ if ($cache = $this->cache->fetch($cacheId)) {
+ $cache = unserialize($cache);
+ }
+ }
+
+ $less = $this->getCompiler();
+ $input = $cache ? $cache : $this->filepath;
+ $cache = $less->cachedCompile($input);
+
+ if (!is_array($input) || $cache['updated'] > $input['updated']) {
+ $this->cache->store($cacheId, serialize($cache));
+ }
+
+ return $this->parsed = $cache;
+ }
+
+ /**
+ * Make a unique cache id for for this source.
+ *
+ * @param string $prefix
+ *
+ * @return string
+ */
+ private function getCacheId($prefix = 'minify') {
+ $md5 = md5($this->filepath);
+ return "{$prefix}_less_{$md5}";
+ }
+
+ /**
+ * Get instance of less compiler
+ *
+ * @return lessc
+ */
+ private function getCompiler() {
+ $less = new lessc();
+ // do not spend CPU time letting less doing minify
+ $less->setPreserveComments(true);
+ return $less;
+ }
+}
diff --git a/min/lib/Minify/ServeConfiguration.php b/min/lib/Minify/ServeConfiguration.php
new file mode 100644
index 0000000..c6c69ab
--- /dev/null
+++ b/min/lib/Minify/ServeConfiguration.php
@@ -0,0 +1,70 @@
+options = $options;
+ $this->sources = $sources;
+ $this->selectionId = $selectionId;
+ }
+
+ /**
+ * @return array
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * @return Minify_SourceInterface[]
+ */
+ public function getSources()
+ {
+ return $this->sources;
+ }
+
+ /**
+ * 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.
+ *
+ * @return string
+ */
+ public function getSelectionId()
+ {
+ return $this->selectionId;
+ }
+}
diff --git a/min/lib/Minify/Source.php b/min/lib/Minify/Source.php
index 9168d6e..34eff47 100644
--- a/min/lib/Minify/Source.php
+++ b/min/lib/Minify/Source.php
@@ -15,142 +15,6 @@
*/
class Minify_Source implements Minify_SourceInterface {
- /**
- * {@inheritdoc}
- */
- public function getLastModified() {
- return $this->lastModified;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getMinifier() {
- return $this->minifier;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setMinifier($minifier) {
- $this->minifier = $minifier;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getMinifierOptions() {
- return $this->minifyOptions;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setMinifierOptions(array $options) {
- $this->minifyOptions = $options;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getContentType() {
- return $this->contentType;
- }
-
- /**
- * 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'];
- }
- }
-
- /**
- * {@inheritdoc}
- */
- 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 ("\xEF\xBB\xBF" === substr($content, 0, 3))
- ? substr($content, 3)
- : $content;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getId()
- {
- return $this->_id;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setupUriRewrites() {
- if ($this->filepath
- && !isset($this->minifyOptions['currentDir'])
- && !isset($this->minifyOptions['prependRelativePath'])
- ) {
- $this->minifyOptions['currentDir'] = dirname($this->filepath);
- }
- }
-
/**
* @var int time of last modification
*/
@@ -179,16 +43,165 @@ class Minify_Source implements Minify_SourceInterface {
/**
* @var string
*/
- protected $_content = null;
+ protected $content = null;
/**
* @var callable
*/
- protected $_getContentFunc = null;
+ protected $getContentFunc = null;
/**
* @var string
*/
- protected $_id = null;
-}
+ protected $id = 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).
+ *
+ * @param array $spec options
+ */
+ public function __construct($spec)
+ {
+ if (isset($spec['filepath'])) {
+ $ext = pathinfo($spec['filepath'], PATHINFO_EXTENSION);
+ switch ($ext) {
+ case 'js' : $this->contentType = Minify::TYPE_JS;
+ break;
+ case 'less' : // fallthrough
+ case 'css' : $this->contentType = Minify::TYPE_CSS;
+ break;
+ case 'htm' : // fallthrough
+ case 'html' : $this->contentType = Minify::TYPE_HTML;
+ break;
+ }
+ $this->filepath = $spec['filepath'];
+ $this->id = $spec['filepath'];
+
+ // TODO ideally not touch disk in constructor
+ $this->lastModified = filemtime($spec['filepath']);
+
+ if (!empty($spec['uploaderHoursBehind'])) {
+ // offset for Windows uploaders with out of sync clocks
+ $this->lastModified += round($spec['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'];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLastModified()
+ {
+ return $this->lastModified;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMinifier()
+ {
+ return $this->minifier;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setMinifier($minifier)
+ {
+ $this->minifier = $minifier;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMinifierOptions()
+ {
+ return $this->minifyOptions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setMinifierOptions(array $options)
+ {
+ $this->minifyOptions = $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContentType()
+ {
+ return $this->contentType;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContent()
+ {
+ if (null === $this->filepath) {
+ if (null === $this->content) {
+ $content = call_user_func($this->getContentFunc, $this->id);
+ } else {
+ $content = $this->content;
+ }
+ } else {
+ $content = file_get_contents($this->filepath);
+ }
+ // remove UTF-8 BOM if present
+ return ("\xEF\xBB\xBF" === substr($content, 0, 3)) ? substr($content, 3) : $content;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilePath()
+ {
+ return $this->filepath;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setupUriRewrites() {
+ if ($this->filepath
+ && !isset($this->minifyOptions['currentDir'])
+ && !isset($this->minifyOptions['prependRelativePath'])
+ ) {
+ $this->minifyOptions['currentDir'] = dirname($this->filepath);
+ }
+ }
+}
diff --git a/min/lib/Minify/Source/Factory.php b/min/lib/Minify/Source/Factory.php
new file mode 100644
index 0000000..4f926ae
--- /dev/null
+++ b/min/lib/Minify/Source/Factory.php
@@ -0,0 +1,175 @@
+env = $env;
+ $this->options = array_merge(array(
+ 'noMinPattern' => '@[-\\.]min\\.(?:[a-zA-Z]+)$@i', // matched against basename
+ 'fileChecker' => array($this, 'checkIsFile'),
+ 'resolveDocRoot' => true,
+ 'checkAllowDirs' => true,
+ 'allowDirs' => array('//'),
+ 'uploaderHoursBehind' => 0,
+ ), $options);
+
+ // resolve // in allowDirs
+ $docRoot = $env->getDocRoot();
+ foreach ($this->options['allowDirs'] as $i => $dir) {
+ $this->options['allowDirs'][$i] = $docRoot . substr($dir, 1);
+ }
+
+ if ($this->options['fileChecker'] && !is_callable($this->options['fileChecker'])) {
+ throw new InvalidArgumentException("fileChecker option is not callable");
+ }
+
+ $this->setHandler('~\.less$~i', function ($spec) use ($cache) {
+ return new Minify_LessCssSource($spec, $cache);
+ });
+
+ $this->setHandler('~\.(js|css)$~i', function ($spec) {
+ return new Minify_Source($spec);
+ });
+ }
+
+ /**
+ * @param string $basenamePattern A pattern tested against basename. E.g. "~\.css$~"
+ * @param callable $handler Function that recieves a $spec array and returns a Minify_SourceInterface
+ */
+ public function setHandler($basenamePattern, $handler)
+ {
+ $this->handlers[$basenamePattern] = $handler;
+ }
+
+ /**
+ * @param string $file
+ * @return string
+ *
+ * @throws Minify_Source_FactoryException
+ */
+ public function checkIsFile($file)
+ {
+ $realpath = realpath($file);
+ if (!$realpath) {
+ throw new Minify_Source_FactoryException("File failed realpath(): $file");
+ }
+
+ $basename = basename($file);
+ if (0 === strpos($basename, '.')) {
+ throw new Minify_Source_FactoryException("Filename starts with period (may be hidden): $basename");
+ }
+
+ if (!is_file($realpath) || !is_readable($realpath)) {
+ throw new Minify_Source_FactoryException("Not a file or isn't readable: $file");
+ }
+
+ return $realpath;
+ }
+
+ /**
+ * @param mixed $spec
+ *
+ * @return Minify_SourceInterface
+ *
+ * @throws Minify_Source_FactoryException
+ */
+ public function makeSource($spec)
+ {
+ $source = null;
+
+ if ($spec instanceof Minify_SourceInterface) {
+ $source = $spec;
+ }
+
+ if (empty($spec['filepath'])) {
+ // not much we can check
+ return new Minify_Source($spec);
+ }
+
+ if ($this->options['resolveDocRoot'] && 0 === strpos($spec['filepath'], '//')) {
+ $spec['filepath'] = $this->env->getDocRoot() . substr($spec['filepath'], 1);
+ }
+
+ if (!empty($this->options['fileChecker'])) {
+ $spec['filepath'] = call_user_func($this->options['fileChecker'], $spec['filepath']);
+ }
+
+ if ($this->options['checkAllowDirs']) {
+ foreach ((array)$this->options['allowDirs'] as $allowDir) {
+ if (strpos($spec['filepath'], $allowDir) !== 0) {
+ throw new Minify_Source_FactoryException("File '{$spec['filepath']}' is outside \$allowDirs."
+ . " If the path is resolved via an alias/symlink, look into the \$min_symlinks option.");
+ }
+ }
+ }
+
+ $basename = basename($spec['filepath']);
+
+ if ($this->options['noMinPattern'] && preg_match($this->options['noMinPattern'], $basename)) {
+ if (preg_match('~\.(css|less)$~i', $basename)) {
+ $spec['minifyOptions']['compress'] = false;
+ // we still want URI rewriting to work for CSS
+ } else {
+ $spec['minifier'] = '';
+ }
+ }
+
+ $hoursBehind = $this->options['uploaderHoursBehind'];
+ if ($hoursBehind != 0) {
+ $spec['uploaderHoursBehind'] = $hoursBehind;
+ }
+
+ foreach ($this->handlers as $basenamePattern => $handler) {
+ if (preg_match($basenamePattern, $basename)) {
+ $source = call_user_func($handler, $spec);
+ break;
+ }
+ }
+
+ if (!$source) {
+ throw new Minify_Source_FactoryException("Handler not found for file: $basename");
+ }
+
+ return $source;
+ }
+}
diff --git a/min/lib/Minify/Source/FactoryException.php b/min/lib/Minify/Source/FactoryException.php
new file mode 100644
index 0000000..0d0ba0e
--- /dev/null
+++ b/min/lib/Minify/Source/FactoryException.php
@@ -0,0 +1,3 @@
+getContentType();
- if ($contentType) {
- return $contentType;
- }
- }
- return 'text/plain';
- }
}
diff --git a/min/quick-test.less b/min/quick-test.less
new file mode 100644
index 0000000..0704552
--- /dev/null
+++ b/min/quick-test.less
@@ -0,0 +1,12 @@
+/*! This file exists only for testing a Minify installation. It's content is not used.
+ *
+ * http://example.org/min/f=min/quick-test.less
+ */
+
+// LESS import statement shares syntax with the CSS import statement.
+// If the file being imported ends in a .less extension, or no extension, then it is treated as a LESS
+// import. Otherwise it is left alone and outputted directly.
+// http://leafo.net/lessphp/docs/#import
+@import "quick-test.css";
+
+@import "quick-testinc.less";
\ No newline at end of file
diff --git a/min/quick-testinc.less b/min/quick-testinc.less
new file mode 100644
index 0000000..c1c33bc
--- /dev/null
+++ b/min/quick-testinc.less
@@ -0,0 +1,20 @@
+@base: 24px;
+@border-color: #B2B;
+
+.underline { border-bottom: 1px solid green }
+
+#header {
+ color: black;
+ border: 1px solid @border-color + #222222;
+
+ .navigation {
+ font-size: @base / 2;
+ a {
+ .underline;
+ }
+ }
+ .logo {
+ width: 300px;
+ :hover { text-decoration: none }
+ }
+}
\ No newline at end of file