initial commit
[namibia] / public / min / lib / Minify / CSS / UriRewriter.php
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  */
13 class 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         // rewrite
71         $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/'
72             ,array(self::$className, '_processUriCB'), $css);
73         $css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
74             ,array(self::$className, '_processUriCB'), $css);
75
76         return $css;
77     }
78     
79     /**
80      * In CSS content, prepend a path to relative URIs
81      * 
82      * @param string $css
83      * 
84      * @param string $path The path to prepend.
85      * 
86      * @return string
87      */
88     public static function prepend($css, $path)
89     {
90         self::$_prependPath = $path;
91         
92         $css = self::_trimUrls($css);
93         
94         // append
95         $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/'
96             ,array(self::$className, '_processUriCB'), $css);
97         $css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
98             ,array(self::$className, '_processUriCB'), $css);
99
100         self::$_prependPath = null;
101         return $css;
102     }
103     
104     /**
105      * Get a root relative URI from a file relative URI
106      *
107      * <code>
108      * Minify_CSS_UriRewriter::rewriteRelative(
109      *       '../img/hello.gif'
110      *     , '/home/user/www/css'  // path of CSS file
111      *     , '/home/user/www'      // doc root
112      * );
113      * // returns '/img/hello.gif'
114      * 
115      * // example where static files are stored in a symlinked directory
116      * Minify_CSS_UriRewriter::rewriteRelative(
117      *       'hello.gif'
118      *     , '/var/staticFiles/theme'
119      *     , '/home/user/www'
120      *     , array('/home/user/www/static' => '/var/staticFiles')
121      * );
122      * // returns '/static/theme/hello.gif'
123      * </code>
124      * 
125      * @param string $uri file relative URI
126      * 
127      * @param string $realCurrentDir realpath of the current file's directory.
128      * 
129      * @param string $realDocRoot realpath of the site document root.
130      * 
131      * @param array $symlinks (default = array()) If the file is stored in 
132      * a symlink-ed directory, provide an array of link paths to
133      * real target paths, where the link paths "appear" to be within the document 
134      * root. E.g.:
135      * <code>
136      * array('/home/foo/www/not/real/path' => '/real/target/path') // unix
137      * array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path')  // Windows
138      * </code>
139      * 
140      * @return string
141      */
142     public static function rewriteRelative($uri, $realCurrentDir, $realDocRoot, $symlinks = array())
143     {
144         // prepend path with current dir separator (OS-independent)
145         $path = strtr($realCurrentDir, '/', DIRECTORY_SEPARATOR)  
146             . DIRECTORY_SEPARATOR . strtr($uri, '/', DIRECTORY_SEPARATOR);
147         
148         self::$debugText .= "file-relative URI  : {$uri}\n"
149                           . "path prepended     : {$path}\n";
150         
151         // "unresolve" a symlink back to doc root
152         foreach ($symlinks as $link => $target) {
153             if (0 === strpos($path, $target)) {
154                 // replace $target with $link
155                 $path = $link . substr($path, strlen($target));
156                 
157                 self::$debugText .= "symlink unresolved : {$path}\n";
158                 
159                 break;
160             }
161         }
162         // strip doc root
163         $path = substr($path, strlen($realDocRoot));
164         
165         self::$debugText .= "docroot stripped   : {$path}\n";
166         
167         // fix to root-relative URI
168         $uri = strtr($path, '/\\', '//');
169         $uri = self::removeDots($uri);
170       
171         self::$debugText .= "traversals removed : {$uri}\n\n";
172         
173         return $uri;
174     }
175
176     /**
177      * Remove instances of "./" and "../" where possible from a root-relative URI
178      *
179      * @param string $uri
180      *
181      * @return string
182      */
183     public static function removeDots($uri)
184     {
185         $uri = str_replace('/./', '/', $uri);
186         // inspired by patch from Oleg Cherniy
187         do {
188             $uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed);
189         } while ($changed);
190         return $uri;
191     }
192     
193     /**
194      * Defines which class to call as part of callbacks, change this
195      * if you extend Minify_CSS_UriRewriter
196      *
197      * @var string
198      */
199     protected static $className = 'Minify_CSS_UriRewriter';
200
201     /**
202      * Get realpath with any trailing slash removed. If realpath() fails,
203      * just remove the trailing slash.
204      * 
205      * @param string $path
206      * 
207      * @return mixed path with no trailing slash
208      */
209     protected static function _realpath($path)
210     {
211         $realPath = realpath($path);
212         if ($realPath !== false) {
213             $path = $realPath;
214         }
215         return rtrim($path, '/\\');
216     }
217
218     /**
219      * Directory of this stylesheet
220      *
221      * @var string
222      */
223     private static $_currentDir = '';
224
225     /**
226      * DOC_ROOT
227      *
228      * @var string
229      */
230     private static $_docRoot = '';
231
232     /**
233      * directory replacements to map symlink targets back to their
234      * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath'
235      *
236      * @var array
237      */
238     private static $_symlinks = array();
239
240     /**
241      * Path to prepend
242      *
243      * @var string
244      */
245     private static $_prependPath = null;
246
247     /**
248      * @param string $css
249      *
250      * @return string
251      */
252     private static function _trimUrls($css)
253     {
254         return preg_replace('/
255             url\\(      # url(
256             \\s*
257             ([^\\)]+?)  # 1 = URI (assuming does not contain ")")
258             \\s*
259             \\)         # )
260         /x', 'url($1)', $css);
261     }
262
263     /**
264      * @param array $m
265      *
266      * @return string
267      */
268     private static function _processUriCB($m)
269     {
270         // $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
271         $isImport = ($m[0][0] === '@');
272         // determine URI and the quote character (if any)
273         if ($isImport) {
274             $quoteChar = $m[1];
275             $uri = $m[2];
276         } else {
277             // $m[1] is either quoted or not
278             $quoteChar = ($m[1][0] === "'" || $m[1][0] === '"')
279                 ? $m[1][0]
280                 : '';
281             $uri = ($quoteChar === '')
282                 ? $m[1]
283                 : substr($m[1], 1, strlen($m[1]) - 2);
284         }
285         // analyze URI
286         if ('/' !== $uri[0]                  // root-relative
287             && false === strpos($uri, '//')  // protocol (non-data)
288             && 0 !== strpos($uri, 'data:')   // data protocol
289         ) {
290             // URI is file-relative: rewrite depending on options
291             if (self::$_prependPath === null) {
292                 $uri = self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks);
293             } else {
294                 $uri = self::$_prependPath . $uri;
295                 if ($uri[0] === '/') {
296                     $root = '';
297                     $rootRelative = $uri;
298                     $uri = $root . self::removeDots($rootRelative);
299                 } elseif (preg_match('@^((https?\:)?//([^/]+))/@', $uri, $m) && (false !== strpos($m[3], '.'))) {
300                     $root = $m[1];
301                     $rootRelative = substr($uri, strlen($root));
302                     $uri = $root . self::removeDots($rootRelative);
303                 }
304             }
305         }
306         return $isImport
307             ? "@import {$quoteChar}{$uri}{$quoteChar}"
308             : "url({$quoteChar}{$uri}{$quoteChar})";
309     }
310 }