1<?php 2/** 3 * Class Minify_CSS_UriRewriter 4 * @package Minify 5 */ 6 7/** 8 * Rewrite file-relative URIs as root-relative in CSS files 9 * 10 * @package Minify 11 * @author Stephen Clay <steve@mrclay.org> 12 */ 13class Minify_CSS_UriRewriter { 14 15 /** 16 * rewrite() and rewriteRelative() append debugging information here 17 * 18 * @var string 19 */ 20 public static $debugText = ''; 21 22 /** 23 * In CSS content, rewrite file relative URIs as root relative 24 * 25 * @param string $css 26 * 27 * @param string $currentDir The directory of the current CSS file. 28 * 29 * @param string $docRoot The document root of the web site in which 30 * the CSS file resides (default = $_SERVER['DOCUMENT_ROOT']). 31 * 32 * @param array $symlinks (default = array()) If the CSS file is stored in 33 * a symlink-ed directory, provide an array of link paths to 34 * target paths, where the link paths are within the document root. Because 35 * paths need to be normalized for this to work, use "//" to substitute 36 * the doc root in the link paths (the array keys). E.g.: 37 * <code> 38 * array('//symlink' => '/real/target/path') // unix 39 * array('//static' => 'D:\\staticStorage') // Windows 40 * </code> 41 * 42 * @return string 43 */ 44 public static function rewrite($css, $currentDir, $docRoot = null, $symlinks = array()) 45 { 46 self::$_docRoot = self::_realpath( 47 $docRoot ? $docRoot : $_SERVER['DOCUMENT_ROOT'] 48 ); 49 self::$_currentDir = self::_realpath($currentDir); 50 self::$_symlinks = array(); 51 52 // normalize symlinks 53 foreach ($symlinks as $link => $target) { 54 $link = ($link === '//') 55 ? self::$_docRoot 56 : str_replace('//', self::$_docRoot . '/', $link); 57 $link = strtr($link, '/', DIRECTORY_SEPARATOR); 58 self::$_symlinks[$link] = self::_realpath($target); 59 } 60 61 self::$debugText .= "docRoot : " . self::$_docRoot . "\n" 62 . "currentDir : " . self::$_currentDir . "\n"; 63 if (self::$_symlinks) { 64 self::$debugText .= "symlinks : " . var_export(self::$_symlinks, 1) . "\n"; 65 } 66 self::$debugText .= "\n"; 67 68 $css = self::_trimUrls($css); 69 70 $css = self::_owlifySvgPaths($css); 71 72 // rewrite 73 $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/' 74 ,array(self::$className, '_processUriCB'), $css); 75 $css = preg_replace_callback('/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/' 76 ,array(self::$className, '_processUriCB'), $css); 77 78 $css = self::_unOwlify($css); 79 80 return $css; 81 } 82 83 /** 84 * In CSS content, prepend a path to relative URIs 85 * 86 * @param string $css 87 * 88 * @param string $path The path to prepend. 89 * 90 * @return string 91 */ 92 public static function prepend($css, $path) 93 { 94 self::$_prependPath = $path; 95 96 $css = self::_trimUrls($css); 97 98 $css = self::_owlifySvgPaths($css); 99 100 // append 101 $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/' 102 ,array(self::$className, '_processUriCB'), $css); 103 $css = preg_replace_callback('/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/' 104 ,array(self::$className, '_processUriCB'), $css); 105 106 $css = self::_unOwlify($css); 107 108 self::$_prependPath = null; 109 return $css; 110 } 111 112 /** 113 * Get a root relative URI from a file relative URI 114 * 115 * <code> 116 * Minify_CSS_UriRewriter::rewriteRelative( 117 * '../img/hello.gif' 118 * , '/home/user/www/css' // path of CSS file 119 * , '/home/user/www' // doc root 120 * ); 121 * // returns '/img/hello.gif' 122 * 123 * // example where static files are stored in a symlinked directory 124 * Minify_CSS_UriRewriter::rewriteRelative( 125 * 'hello.gif' 126 * , '/var/staticFiles/theme' 127 * , '/home/user/www' 128 * , array('/home/user/www/static' => '/var/staticFiles') 129 * ); 130 * // returns '/static/theme/hello.gif' 131 * </code> 132 * 133 * @param string $uri file relative URI 134 * 135 * @param string $realCurrentDir realpath of the current file's directory. 136 * 137 * @param string $realDocRoot realpath of the site document root. 138 * 139 * @param array $symlinks (default = array()) If the file is stored in 140 * a symlink-ed directory, provide an array of link paths to 141 * real target paths, where the link paths "appear" to be within the document 142 * root. E.g.: 143 * <code> 144 * array('/home/foo/www/not/real/path' => '/real/target/path') // unix 145 * array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path') // Windows 146 * </code> 147 * 148 * @return string 149 */ 150 public static function rewriteRelative($uri, $realCurrentDir, $realDocRoot, $symlinks = array()) 151 { 152 // prepend path with current dir separator (OS-independent) 153 $path = strtr($realCurrentDir, '/', DIRECTORY_SEPARATOR) 154 . DIRECTORY_SEPARATOR . strtr($uri, '/', DIRECTORY_SEPARATOR); 155 156 self::$debugText .= "file-relative URI : {$uri}\n" 157 . "path prepended : {$path}\n"; 158 159 // "unresolve" a symlink back to doc root 160 foreach ($symlinks as $link => $target) { 161 if (0 === strpos($path, $target)) { 162 // replace $target with $link 163 $path = $link . substr($path, strlen($target)); 164 165 self::$debugText .= "symlink unresolved : {$path}\n"; 166 167 break; 168 } 169 } 170 // strip doc root 171 $path = substr($path, strlen($realDocRoot)); 172 173 self::$debugText .= "docroot stripped : {$path}\n"; 174 175 // fix to root-relative URI 176 $uri = strtr($path, '/\\', '//'); 177 $uri = self::removeDots($uri); 178 179 self::$debugText .= "traversals removed : {$uri}\n\n"; 180 181 return $uri; 182 } 183 184 /** 185 * Remove instances of "./" and "../" where possible from a root-relative URI 186 * 187 * @param string $uri 188 * 189 * @return string 190 */ 191 public static function removeDots($uri) 192 { 193 $uri = str_replace('/./', '/', $uri); 194 // inspired by patch from Oleg Cherniy 195 do { 196 $uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed); 197 } while ($changed); 198 return $uri; 199 } 200 201 /** 202 * Defines which class to call as part of callbacks, change this 203 * if you extend Minify_CSS_UriRewriter 204 * 205 * @var string 206 */ 207 protected static $className = 'Minify_CSS_UriRewriter'; 208 209 /** 210 * Get realpath with any trailing slash removed. If realpath() fails, 211 * just remove the trailing slash. 212 * 213 * @param string $path 214 * 215 * @return mixed path with no trailing slash 216 */ 217 protected static function _realpath($path) 218 { 219 $realPath = realpath($path); 220 if ($realPath !== false) { 221 $path = $realPath; 222 } 223 return rtrim($path, '/\\'); 224 } 225 226 /** 227 * Directory of this stylesheet 228 * 229 * @var string 230 */ 231 private static $_currentDir = ''; 232 233 /** 234 * DOC_ROOT 235 * 236 * @var string 237 */ 238 private static $_docRoot = ''; 239 240 /** 241 * directory replacements to map symlink targets back to their 242 * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath' 243 * 244 * @var array 245 */ 246 private static $_symlinks = array(); 247 248 /** 249 * Path to prepend 250 * 251 * @var string 252 */ 253 private static $_prependPath = null; 254 255 /** 256 * @param string $css 257 * 258 * @return string 259 */ 260 private static function _trimUrls($css) 261 { 262 return preg_replace('/ 263 url\\( # url( 264 \\s* 265 ([^\\)]+?) # 1 = URI (assuming does not contain ")") 266 \\s* 267 \\) # ) 268 /x', 'url($1)', $css); 269 } 270 271 /** 272 * @param array $m 273 * 274 * @return string 275 */ 276 private static function _processUriCB($m) 277 { 278 // $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' 279 $isImport = ($m[0][0] === '@'); 280 // determine URI and the quote character (if any) 281 if ($isImport) { 282 $quoteChar = $m[1]; 283 $uri = $m[2]; 284 } else { 285 // $m[1] is either quoted or not 286 $quoteChar = ($m[1][0] === "'" || $m[1][0] === '"') 287 ? $m[1][0] 288 : ''; 289 $uri = ($quoteChar === '') 290 ? $m[1] 291 : substr($m[1], 1, strlen($m[1]) - 2); 292 } 293 294 if ($uri === '') { 295 return $m[0]; 296 } 297 298 // if not root/scheme relative and not starts with scheme 299 if (!preg_match('~^(/|[a-z]+\:)~', $uri)) { 300 // URI is file-relative: rewrite depending on options 301 if (self::$_prependPath === null) { 302 $uri = self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks); 303 } else { 304 $uri = self::$_prependPath . $uri; 305 if ($uri[0] === '/') { 306 $root = ''; 307 $rootRelative = $uri; 308 $uri = $root . self::removeDots($rootRelative); 309 } elseif (preg_match('@^((https?\:)?//([^/]+))/@', $uri, $m) && (false !== strpos($m[3], '.'))) { 310 $root = $m[1]; 311 $rootRelative = substr($uri, strlen($root)); 312 $uri = $root . self::removeDots($rootRelative); 313 } 314 } 315 } 316 return $isImport 317 ? "@import {$quoteChar}{$uri}{$quoteChar}" 318 : "url({$quoteChar}{$uri}{$quoteChar})"; 319 } 320 321 /** 322 * Mungs some inline SVG URL declarations so they won't be touched 323 * 324 * @link https://github.com/mrclay/minify/issues/517 325 * @see _unOwlify 326 * 327 * @param string $css 328 * @return string 329 */ 330 private static function _owlifySvgPaths($css) { 331 return preg_replace('~\b((?:clip-path|mask|-webkit-mask)\s*\:\s*)url(\(\s*#\w+\s*\))~', '$1owl$2', $css); 332 333 } 334 335 /** 336 * Undo work of _owlify 337 * 338 * @see _owlifySvgPaths 339 * 340 * @param string $css 341 * @return string 342 */ 343 private static function _unOwlify($css) { 344 return preg_replace('~\b((?:clip-path|mask|-webkit-mask)\s*\:\s*)owl~', '$1url', $css); 345 } 346} 347