initial commit
[namibia] / public / min / lib / Minify / ImportProcessor.php
1 <?php
2 /**
3  * Class Minify_ImportProcessor
4  * @package Minify
5  */
6
7 /**
8  * Linearize a CSS/JS file by including content specified by CSS import
9  * declarations. In CSS files, relative URIs are fixed.
10  *
11  * @imports will be processed regardless of where they appear in the source
12  * files; i.e. @imports commented out or in string content will still be
13  * processed!
14  *
15  * This has a unit test but should be considered "experimental".
16  *
17  * @package Minify
18  * @author Stephen Clay <steve@mrclay.org>
19  * @author Simon Schick <simonsimcity@gmail.com>
20  */
21 class Minify_ImportProcessor {
22
23     public static $filesIncluded = array();
24
25     public static function process($file)
26     {
27         self::$filesIncluded = array();
28         self::$_isCss = (strtolower(substr($file, -4)) === '.css');
29         $obj = new Minify_ImportProcessor(dirname($file));
30         return $obj->_getContent($file);
31     }
32
33     // allows callback funcs to know the current directory
34     private $_currentDir = null;
35
36     // allows callback funcs to know the directory of the file that inherits this one
37     private $_previewsDir = null;
38
39     // allows _importCB to write the fetched content back to the obj
40     private $_importedContent = '';
41
42     private static $_isCss = null;
43
44     /**
45      * @param String $currentDir
46      * @param String $previewsDir Is only used internally
47      */
48     private function __construct($currentDir, $previewsDir = "")
49     {
50         $this->_currentDir = $currentDir;
51         $this->_previewsDir = $previewsDir;
52     }
53
54     private function _getContent($file, $is_imported = false)
55     {
56         $file = realpath($file);
57         if (! $file
58             || in_array($file, self::$filesIncluded)
59             || false === ($content = @file_get_contents($file))
60         ) {
61             // file missing, already included, or failed read
62             return '';
63         }
64         self::$filesIncluded[] = realpath($file);
65         $this->_currentDir = dirname($file);
66
67         // remove UTF-8 BOM if present
68         if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) {
69             $content = substr($content, 3);
70         }
71         // ensure uniform EOLs
72         $content = str_replace("\r\n", "\n", $content);
73
74         // process @imports
75         $content = preg_replace_callback(
76             '/
77                 @import\\s+
78                 (?:url\\(\\s*)?      # maybe url(
79                 [\'"]?               # maybe quote
80                 (.*?)                # 1 = URI
81                 [\'"]?               # maybe end quote
82                 (?:\\s*\\))?         # maybe )
83                 ([a-zA-Z,\\s]*)?     # 2 = media list
84                 ;                    # end token
85             /x'
86             ,array($this, '_importCB')
87             ,$content
88         );
89
90         // You only need to rework the import-path if the script is imported
91         if (self::$_isCss && $is_imported) {
92             // rewrite remaining relative URIs
93             $content = preg_replace_callback(
94                 '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
95                 ,array($this, '_urlCB')
96                 ,$content
97             );
98         }
99
100         return $this->_importedContent . $content;
101     }
102
103     private function _importCB($m)
104     {
105         $url = $m[1];
106         $mediaList = preg_replace('/\\s+/', '', $m[2]);
107
108         if (strpos($url, '://') > 0) {
109             // protocol, leave in place for CSS, comment for JS
110             return self::$_isCss
111                 ? $m[0]
112                 : "/* Minify_ImportProcessor will not include remote content */";
113         }
114         if ('/' === $url[0]) {
115             // protocol-relative or root path
116             $url = ltrim($url, '/');
117             $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR
118                 . strtr($url, '/', DIRECTORY_SEPARATOR);
119         } else {
120             // relative to current path
121             $file = $this->_currentDir . DIRECTORY_SEPARATOR
122                 . strtr($url, '/', DIRECTORY_SEPARATOR);
123         }
124         $obj = new Minify_ImportProcessor(dirname($file), $this->_currentDir);
125         $content = $obj->_getContent($file, true);
126         if ('' === $content) {
127             // failed. leave in place for CSS, comment for JS
128             return self::$_isCss
129                 ? $m[0]
130                 : "/* Minify_ImportProcessor could not fetch '{$file}' */";
131         }
132         return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList))
133             ? $content
134             : "@media {$mediaList} {\n{$content}\n}\n";
135     }
136
137     private function _urlCB($m)
138     {
139         // $m[1] is either quoted or not
140         $quote = ($m[1][0] === "'" || $m[1][0] === '"')
141             ? $m[1][0]
142             : '';
143         $url = ($quote === '')
144             ? $m[1]
145             : substr($m[1], 1, strlen($m[1]) - 2);
146         if ('/' !== $url[0]) {
147             if (strpos($url, '//') > 0) {
148                 // probably starts with protocol, do not alter
149             } else {
150                 // prepend path with current dir separator (OS-independent)
151                 $path = $this->_currentDir
152                     . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR);
153                 // update the relative path by the directory of the file that imported this one
154                 $url = self::getPathDiff(realpath($this->_previewsDir), $path);
155             }
156         }
157         return "url({$quote}{$url}{$quote})";
158     }
159
160     /**
161      * @param string $from
162      * @param string $to
163      * @param string $ps
164      * @return string
165      */
166     private function getPathDiff($from, $to, $ps = DIRECTORY_SEPARATOR)
167     {
168         $realFrom = $this->truepath($from);
169         $realTo = $this->truepath($to);
170
171         $arFrom = explode($ps, rtrim($realFrom, $ps));
172         $arTo = explode($ps, rtrim($realTo, $ps));
173         while (count($arFrom) && count($arTo) && ($arFrom[0] == $arTo[0]))
174         {
175             array_shift($arFrom);
176             array_shift($arTo);
177         }
178         return str_pad("", count($arFrom) * 3, '..' . $ps) . implode($ps, $arTo);
179     }
180
181     /**
182      * This function is to replace PHP's extremely buggy realpath().
183      * @param string $path The original path, can be relative etc.
184      * @return string The resolved path, it might not exist.
185      * @see http://stackoverflow.com/questions/4049856/replace-phps-realpath
186      */
187     function truepath($path)
188     {
189         // whether $path is unix or not
190         $unipath = strlen($path) == 0 || $path{0} != '/';
191         // attempts to detect if path is relative in which case, add cwd
192         if (strpos($path, ':') === false && $unipath)
193             $path = $this->_currentDir . DIRECTORY_SEPARATOR . $path;
194
195         // resolve path parts (single dot, double dot and double delimiters)
196         $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
197         $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
198         $absolutes = array();
199         foreach ($parts as $part) {
200             if ('.' == $part)
201                 continue;
202             if ('..' == $part) {
203                 array_pop($absolutes);
204             } else {
205                 $absolutes[] = $part;
206             }
207         }
208         $path = implode(DIRECTORY_SEPARATOR, $absolutes);
209         // resolve any symlinks
210         if (file_exists($path) && linkinfo($path) > 0)
211             $path = readlink($path);
212         // put initial separator that could have been lost
213         $path = !$unipath ? '/' . $path : $path;
214         return $path;
215     }
216 }