| 1 |
<?php |
| 2 |
/** |
| 3 |
* Class Minify_HTML_Helper |
| 4 |
* @package Minify |
| 5 |
*/ |
| 6 |
|
| 7 |
/** |
| 8 |
* Helpers for writing Minify URIs into HTML |
| 9 |
* |
| 10 |
* @package Minify |
| 11 |
* @author Stephen Clay <steve@mrclay.org> |
| 12 |
*/ |
| 13 |
class Minify_HTML_Helper |
| 14 |
{ |
| 15 |
public $rewriteWorks = true; |
| 16 |
public $minAppUri = '/min'; |
| 17 |
public $groupsConfigFile = ''; |
| 18 |
|
| 19 |
/** |
| 20 |
* Get an HTML-escaped Minify URI for a group or set of files |
| 21 |
* |
| 22 |
* @param string|array $keyOrFiles a group key or array of filepaths/URIs |
| 23 |
* @param array $opts options: |
| 24 |
* 'farExpires' : (default true) append a modified timestamp for cache revving |
| 25 |
* 'debug' : (default false) append debug flag |
| 26 |
* 'charset' : (default 'UTF-8') for htmlspecialchars |
| 27 |
* 'minAppUri' : (default '/min') URI of min directory |
| 28 |
* 'rewriteWorks' : (default true) does mod_rewrite work in min app? |
| 29 |
* 'groupsConfigFile' : specify if different |
| 30 |
* @return string |
| 31 |
*/ |
| 32 |
public static function getUri($keyOrFiles, $opts = array()) |
| 33 |
{ |
| 34 |
$opts = array_merge(array( // default options |
| 35 |
'farExpires' => true, |
| 36 |
'debug' => false, |
| 37 |
'charset' => 'UTF-8', |
| 38 |
'minAppUri' => '/min', |
| 39 |
'rewriteWorks' => true, |
| 40 |
'groupsConfigFile' => self::app()->groupsConfigPath, |
| 41 |
), $opts); |
| 42 |
|
| 43 |
$h = new self; |
| 44 |
$h->minAppUri = $opts['minAppUri']; |
| 45 |
$h->rewriteWorks = $opts['rewriteWorks']; |
| 46 |
$h->groupsConfigFile = $opts['groupsConfigFile']; |
| 47 |
|
| 48 |
if (is_array($keyOrFiles)) { |
| 49 |
$h->setFiles($keyOrFiles, $opts['farExpires']); |
| 50 |
} else { |
| 51 |
$h->setGroup($keyOrFiles, $opts['farExpires']); |
| 52 |
} |
| 53 |
$uri = $h->getRawUri($opts['farExpires'], $opts['debug']); |
| 54 |
|
| 55 |
return htmlspecialchars($uri, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, $opts['charset']); |
| 56 |
} |
| 57 |
|
| 58 |
/** |
| 59 |
* Get non-HTML-escaped URI to minify the specified files |
| 60 |
* |
| 61 |
* @param bool $farExpires |
| 62 |
* @param bool $debug |
| 63 |
* @return string |
| 64 |
*/ |
| 65 |
public function getRawUri($farExpires = true, $debug = false) |
| 66 |
{ |
| 67 |
$path = rtrim($this->minAppUri, '/') . '/'; |
| 68 |
if (! $this->rewriteWorks) { |
| 69 |
$path .= '?'; |
| 70 |
} |
| 71 |
if (null === $this->_groupKey) { |
| 72 |
// @todo: implement shortest uri |
| 73 |
$path = self::_getShortestUri($this->_filePaths, $path); |
| 74 |
} else { |
| 75 |
$path .= "g=" . $this->_groupKey; |
| 76 |
} |
| 77 |
if ($debug) { |
| 78 |
$path .= "&debug"; |
| 79 |
} elseif ($farExpires && $this->_lastModified) { |
| 80 |
$path .= "&" . $this->_lastModified; |
| 81 |
} |
| 82 |
|
| 83 |
return $path; |
| 84 |
} |
| 85 |
|
| 86 |
/** |
| 87 |
* Set the files that will comprise the URI we're building |
| 88 |
* |
| 89 |
* @param array $files |
| 90 |
* @param bool $checkLastModified |
| 91 |
*/ |
| 92 |
public function setFiles($files, $checkLastModified = true) |
| 93 |
{ |
| 94 |
$this->_groupKey = null; |
| 95 |
if ($checkLastModified) { |
| 96 |
$this->_lastModified = self::getLastModified($files); |
| 97 |
} |
| 98 |
// normalize paths like in /min/f=<paths> |
| 99 |
foreach ($files as $k => $file) { |
| 100 |
if (0 === strpos($file, '//')) { |
| 101 |
$file = substr($file, 2); |
| 102 |
} elseif (0 === strpos($file, '/') || 1 === strpos($file, ':\\')) { |
| 103 |
$file = substr($file, strlen(self::app()->env->getDocRoot()) + 1); |
| 104 |
} |
| 105 |
$file = strtr($file, '\\', '/'); |
| 106 |
$files[$k] = $file; |
| 107 |
} |
| 108 |
$this->_filePaths = $files; |
| 109 |
} |
| 110 |
|
| 111 |
/** |
| 112 |
* Set the group of files that will comprise the URI we're building |
| 113 |
* |
| 114 |
* @param string $key |
| 115 |
* @param bool $checkLastModified |
| 116 |
*/ |
| 117 |
public function setGroup($key, $checkLastModified = true) |
| 118 |
{ |
| 119 |
$this->_groupKey = $key; |
| 120 |
if ($checkLastModified) { |
| 121 |
if (! $this->groupsConfigFile) { |
| 122 |
$this->groupsConfigFile = self::app()->groupsConfigPath; |
| 123 |
} |
| 124 |
if (is_file($this->groupsConfigFile)) { |
| 125 |
$gc = (require $this->groupsConfigFile); |
| 126 |
$keys = explode(',', $key); |
| 127 |
foreach ($keys as $key) { |
| 128 |
if (!isset($gc[$key])) { |
| 129 |
// this can happen if value is null |
| 130 |
// which could be solved with array_filter |
| 131 |
continue; |
| 132 |
} |
| 133 |
|
| 134 |
$this->_lastModified = self::getLastModified($gc[$key], $this->_lastModified); |
| 135 |
} |
| 136 |
} |
| 137 |
} |
| 138 |
} |
| 139 |
|
| 140 |
/** |
| 141 |
* Get the max(lastModified) of all files |
| 142 |
* |
| 143 |
* @param array|string $sources |
| 144 |
* @param int $lastModified |
| 145 |
* @return int |
| 146 |
*/ |
| 147 |
public static function getLastModified($sources, $lastModified = 0) |
| 148 |
{ |
| 149 |
$max = $lastModified; |
| 150 |
$factory = self::app()->sourceFactory; |
| 151 |
|
| 152 |
/** @var Minify_Source $source */ |
| 153 |
foreach ((array)$sources as $source) { |
| 154 |
$source = $factory->makeSource($source); |
| 155 |
$max = max($max, $source->getLastModified()); |
| 156 |
} |
| 157 |
|
| 158 |
return $max; |
| 159 |
} |
| 160 |
|
| 161 |
/** |
| 162 |
* @param \Minify\App $app |
| 163 |
* @return \Minify\App |
| 164 |
* @internal |
| 165 |
*/ |
| 166 |
public static function app(?\Minify\App $app = null) |
| 167 |
{ |
| 168 |
static $cached; |
| 169 |
if ($app) { |
| 170 |
$cached = $app; |
| 171 |
|
| 172 |
return $app; |
| 173 |
} |
| 174 |
if ($cached === null) { |
| 175 |
$cached = (require __DIR__ . '/../../../bootstrap.php'); |
| 176 |
} |
| 177 |
|
| 178 |
return $cached; |
| 179 |
} |
| 180 |
|
| 181 |
protected $_groupKey = null; // if present, URI will be like g=... |
| 182 |
protected $_filePaths = array(); |
| 183 |
protected $_lastModified = null; |
| 184 |
|
| 185 |
/** |
| 186 |
* In a given array of strings, find the character they all have at |
| 187 |
* a particular index |
| 188 |
* |
| 189 |
* @param array $arr array of strings |
| 190 |
* @param int $pos index to check |
| 191 |
* @return mixed a common char or '' if any do not match |
| 192 |
*/ |
| 193 |
protected static function _getCommonCharAtPos($arr, $pos) |
| 194 |
{ |
| 195 |
if (!isset($arr[0][$pos])) { |
| 196 |
return ''; |
| 197 |
} |
| 198 |
$c = $arr[0][$pos]; |
| 199 |
$l = count($arr); |
| 200 |
if ($l === 1) { |
| 201 |
return $c; |
| 202 |
} |
| 203 |
for ($i = 1; $i < $l; ++$i) { |
| 204 |
if ($arr[$i][$pos] !== $c) { |
| 205 |
return ''; |
| 206 |
} |
| 207 |
} |
| 208 |
|
| 209 |
return $c; |
| 210 |
} |
| 211 |
|
| 212 |
/** |
| 213 |
* Get the shortest URI to minify the set of source files |
| 214 |
* |
| 215 |
* @param array $paths root-relative URIs of files |
| 216 |
* @param string $minRoot root-relative URI of the "min" application |
| 217 |
* @return string |
| 218 |
*/ |
| 219 |
protected static function _getShortestUri($paths, $minRoot = '/min/') |
| 220 |
{ |
| 221 |
$pos = 0; |
| 222 |
$base = ''; |
| 223 |
while (true) { |
| 224 |
$c = self::_getCommonCharAtPos($paths, $pos); |
| 225 |
if ($c === '') { |
| 226 |
break; |
| 227 |
} else { |
| 228 |
$base .= $c; |
| 229 |
} |
| 230 |
++$pos; |
| 231 |
} |
| 232 |
$base = preg_replace('@[^/]+$@', '', $base); |
| 233 |
$uri = $minRoot . 'f=' . implode(',', $paths); |
| 234 |
|
| 235 |
if (substr($base, -1) === '/') { |
| 236 |
// we have a base dir! |
| 237 |
$basedPaths = $paths; |
| 238 |
$l = count($paths); |
| 239 |
for ($i = 0; $i < $l; ++$i) { |
| 240 |
$basedPaths[$i] = substr($paths[$i], strlen($base)); |
| 241 |
} |
| 242 |
$base = substr($base, 0, strlen($base) - 1); |
| 243 |
$bUri = $minRoot . 'b=' . $base . '&f=' . implode(',', $basedPaths); |
| 244 |
|
| 245 |
$uri = strlen($uri) < strlen($bUri) ? $uri : $bUri; |
| 246 |
} |
| 247 |
|
| 248 |
return $uri; |
| 249 |
} |
| 250 |
} |
| 251 |
|