summaryrefslogtreecommitdiffstats
path: root/admin/survey/minify/lib/Minify/ImportProcessor.php
blob: 0f8c981b21ee8c41a85bff3fd62b0e440eea9160 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
<?php
/**
 * Class Minify_ImportProcessor
 * @package Minify
 */

/**
 * Linearize a CSS/JS file by including content specified by CSS import
 * declarations. In CSS files, relative URIs are fixed.
 *
 * @imports will be processed regardless of where they appear in the source
 * files; i.e. @imports commented out or in string content will still be
 * processed!
 *
 * This has a unit test but should be considered "experimental".
 *
 * @package Minify
 * @author Stephen Clay <steve@mrclay.org>
 * @author Simon Schick <simonsimcity@gmail.com>
 */
class Minify_ImportProcessor
{
    public static $filesIncluded = array();

    public static function process($file)
    {
        self::$filesIncluded = array();
        self::$_isCss = (strtolower(substr($file, -4)) === '.css');
        $obj = new Minify_ImportProcessor(dirname($file));

        return $obj->_getContent($file);
    }

    // allows callback funcs to know the current directory
    private $_currentDir;

    // allows callback funcs to know the directory of the file that inherits this one
    private $_previewsDir;

    // allows _importCB to write the fetched content back to the obj
    private $_importedContent = '';

    private static $_isCss;

    /**
     * @param String $currentDir
     * @param String $previewsDir Is only used internally
     */
    private function __construct($currentDir, $previewsDir = "")
    {
        $this->_currentDir = $currentDir;
        $this->_previewsDir = $previewsDir;
    }

    private function _getContent($file, $is_imported = false)
    {
        $file = preg_replace('~\\?.*~', '', $file);
        $file = realpath($file);
        if (! $file
            || in_array($file, self::$filesIncluded)
            || false === ($content = @file_get_contents($file))) {
            // file missing, already included, or failed read
            return '';
        }
        self::$filesIncluded[] = realpath($file);
        $this->_currentDir = dirname($file);

        // remove UTF-8 BOM if present
        if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) {
            $content = substr($content, 3);
        }
        // ensure uniform EOLs
        $content = str_replace("\r\n", "\n", $content);

        // process @imports
        $pattern = '/
                @import\\s+
                (?:url\\(\\s*)?      # maybe url(
                [\'"]?               # maybe quote
                (.*?)                # 1 = URI
                [\'"]?               # maybe end quote
                (?:\\s*\\))?         # maybe )
                ([a-zA-Z,\\s]*)?     # 2 = media list
                ;                    # end token
            /x';
        $content = preg_replace_callback($pattern, array($this, '_importCB'), $content);

        // You only need to rework the import-path if the script is imported
        if (self::$_isCss && $is_imported) {
            // rewrite remaining relative URIs
            $pattern = '/url\\(\\s*([^\\)\\s]+)\\s*\\)/';
            $content = preg_replace_callback($pattern, array($this, '_urlCB'), $content);
        }

        return $this->_importedContent . $content;
    }

    private function _importCB($m)
    {
        $url = $m[1];
        $mediaList = preg_replace('/\\s+/', '', $m[2]);

        if (strpos($url, '://') > 0) {
            // protocol, leave in place for CSS, comment for JS
            return self::$_isCss
                ? $m[0]
                : "/* Minify_ImportProcessor will not include remote content */";
        }
        if ('/' === $url[0]) {
            // protocol-relative or root path
            $url = ltrim($url, '/');
            $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR
                . strtr($url, '/', DIRECTORY_SEPARATOR);
        } else {
            // relative to current path
            $file = $this->_currentDir . DIRECTORY_SEPARATOR
                . strtr($url, '/', DIRECTORY_SEPARATOR);
        }
        $obj = new Minify_ImportProcessor(dirname($file), $this->_currentDir);
        $content = $obj->_getContent($file, true);
        if ('' === $content) {
            // failed. leave in place for CSS, comment for JS
            return self::$_isCss
                ? $m[0]
                : "/* Minify_ImportProcessor could not fetch '{$file}' */";
        }

        return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList))
            ? $content
            : "@media {$mediaList} {\n{$content}\n}\n";
    }

    private function _urlCB($m)
    {
        // $m[1] is either quoted or not
        $quote = ($m[1][0] === "'" || $m[1][0] === '"') ? $m[1][0] : '';

        $url = ($quote === '') ? $m[1] : substr($m[1], 1, strlen($m[1]) - 2);

        if ('/' !== $url[0]) {
            if (strpos($url, '//') > 0) {
                // probably starts with protocol, do not alter
            } else {
                // prepend path with current dir separator (OS-independent)
                $path = $this->_currentDir
                    . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR);
                // update the relative path by the directory of the file that imported this one
                $url = self::getPathDiff(realpath($this->_previewsDir), $path);
            }
        }

        return "url({$quote}{$url}{$quote})";
    }

    /**
     * @param string $from
     * @param string $to
     * @param string $ps
     * @return string
     */
    private function getPathDiff($from, $to, $ps = DIRECTORY_SEPARATOR)
    {
        $realFrom = $this->truepath($from);
        $realTo = $this->truepath($to);

        $arFrom = explode($ps, rtrim($realFrom, $ps));
        $arTo = explode($ps, rtrim($realTo, $ps));
        while (count($arFrom) && count($arTo) && ($arFrom[0] == $arTo[0])) {
            array_shift($arFrom);
            array_shift($arTo);
        }

        return str_pad("", count($arFrom) * 3, '..' . $ps) . implode($ps, $arTo);
    }

    /**
     * This function is to replace PHP's extremely buggy realpath().
     * @param string $path The original path, can be relative etc.
     * @return string The resolved path, it might not exist.
     * @see http://stackoverflow.com/questions/4049856/replace-phps-realpath
     */
    private function truepath($path)
    {
        // whether $path is unix or not
        $unipath = ('' === $path) || ($path{0} !== '/');

        // attempts to detect if path is relative in which case, add cwd
        if (strpos($path, ':') === false && $unipath) {
            $path = $this->_currentDir . DIRECTORY_SEPARATOR . $path;
        }

        // resolve path parts (single dot, double dot and double delimiters)
        $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
        $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
        $absolutes = array();
        foreach ($parts as $part) {
            if ('.' === $part) {
                continue;
            }
            if ('..' === $part) {
                array_pop($absolutes);
            } else {
                $absolutes[] = $part;
            }
        }

        $path = implode(DIRECTORY_SEPARATOR, $absolutes);
        // resolve any symlinks
        if (file_exists($path) && linkinfo($path) > 0) {
            $path = readlink($path);
        }
        // put initial separator that could have been lost
        $path = !$unipath ? '/' . $path : $path;

        return $path;
    }
}