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