Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageSchema
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 10
1892
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 registerCSS
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 registerJS
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
56
 getCSSIncludes
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getJSTopIncludes
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getJSBottomIncludes
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 mergeIncludes
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
132
 build
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 buildGroup
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 write_minified_file
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace UserFrosting;
4
5/* A class representing the css and js to load for a given page. */
6/* For minification, the `build` tool (TODO) will need to be re-run any time a CSS/JS include is added or removed, including those from plugin installation */
7    
8class PageSchema {
9
10    protected $_css_uri;    // the root URI of the css directory
11    protected $_css_path;   // the root filesystem path of the css directory
12    protected $_js_uri;     // the root URI of the js directory
13    protected $_js_path;    // the root filesystem path of the js directory
14
15    protected $_css_includes = [];           // An multidimensional array of css includes, keyed by page group name, each which contains a list of CSS files
16    protected $_js_includes_top = [];        // An multidimensional array of js includes, keyed by page group name, each which contains a list of JS files
17    protected $_js_includes_bottom = [];     // An multidimensional array of js includes, keyed by page group name, each which contains a list of JS files
18    
19    public function __construct($css_uri, $css_path, $js_uri, $js_path){
20        $this->_css_uri =  $css_uri;
21        $this->_css_path = $css_path;
22        $this->_js_uri =  $js_uri;
23        $this->_js_path = $js_path;        
24    }
25    
26    // Register a CSS include for a specified page group
27    public function registerCSS($group_name, $path){
28        if (!isset($this->_css_includes[$group_name]))
29            $this->_css_includes[$group_name] = [];
30            
31        if (!in_array($path, $this->_css_includes))
32            $this->_css_includes[$group_name][] = $path;
33    }
34
35    // Register a JS include for a specified page group
36    public function registerJS($group_name, $path, $position = "bottom"){
37        if ($position == "bottom"){
38            if (!isset($this->_js_includes_bottom[$group_name]))
39                $this->_js_includes_bottom[$group_name] = [];
40            
41            if (!in_array($path, $this->_js_includes_bottom[$group_name]))
42                $this->_js_includes_bottom[$group_name][] = $path;
43        } else if ($position == "top"){
44            if (!isset($this->_js_includes_top[$group_name]))
45                $this->_js_includes_top[$group_name] = [];
46            
47            if (!in_array($path, $this->_js_includes_top[$group_name]))    
48                $this->_js_includes_top[$group_name][] = $path;
49        } else {
50            throw new \Exception("'position' must be either 'top' or 'bottom'.");
51        }
52    }
53
54    // Get the CSS includes for a specified page group
55    public function getCSSIncludes($group_name = "common", $minify = false) {
56        // Check if the specified group actually exists, otherwise use the common minified file.
57        if (isset($this->_css_includes[$group_name]))
58            $minfile = "min/$group_name.min.css";
59        else
60            $minfile = "min/common.min.css";
61        return $this->mergeIncludes($this->_css_uri, $this->_css_includes, $group_name, $minfile , $minify);
62    }
63    
64   // Get the header JS includes for a specified page group
65    public function getJSTopIncludes($group_name = "common", $minify = false) {
66        // Check if the specified group actually exists, otherwise use nothing.
67        if (isset($this->_js_includes_top[$group_name]))
68            $minfile = "min/$group_name-top.min.js";
69        else {
70            $minfile = "";
71            $minify = false;
72        }
73        return $this->mergeIncludes($this->_js_uri, $this->_js_includes_top, $group_name, $minfile, $minify);
74    }    
75
76    // Get the footer JS includes for a specified page group
77    public function getJSBottomIncludes($group_name = "common", $minify = false) {
78        // Check if the specified group actually exists, otherwise use the common minified file.
79        if (isset($this->_js_includes_bottom[$group_name]))
80            $minfile = "min/$group_name-bottom.min.js";
81        else
82            $minfile = "min/common-bottom.min.js";
83        return $this->mergeIncludes($this->_js_uri, $this->_js_includes_bottom, $group_name, $minfile, $minify);
84    }
85    
86    private function mergeIncludes($root, $raw, $group_name, $minfile, $minify = false, $include_externals = true){
87        // Combine the common and group-specific includes    
88        if (isset($raw["common"]))
89            $includes = $raw["common"];
90        else
91            $includes = [];
92        if ($group_name != "common" && isset($raw[$group_name]))
93            $includes = array_merge($includes, $raw[$group_name]);
94 
95        // For minified, replace with minified file but we still need to include any external includes
96        if ($minify){
97            $includes_parsed = [$root . $minfile];
98            // Include external files
99            if ($include_externals) {
100                foreach ($includes as $path){
101                    if (strpos($path, 'http') === 0)
102                        $includes_parsed[] = $path;
103                }
104            }
105        } else {        
106            $includes_parsed = [];
107            foreach ($includes as $path){
108                if (strpos($path, 'http') === 0) {
109                    if ($include_externals)
110                        $includes_parsed[] = $path;
111                } else
112                    $includes_parsed[] = $root . $path;
113            }
114        }
115        return $includes_parsed;
116    }
117    
118    
119    // Builds the minified CSS and JS files for the site.  This function uses the Yahoo User Interface (YUI) interface (http://yui.github.io/yuicompressor/) to compress and minify the files as specified in this PageSchema object.
120    public function build($debug = false){
121        // Determine path to YUI jar file
122        $yui = __DIR__ . "/yuicompressor-2.4.8.jar";      
123        
124        /*
125         * In XAMPP and Mac OSX, by default Apache is run under the user 'daemon'.  To grant proper write permissions, run the following shell command:
126         * `sudo chown -R daemon:daemon <write_path>`
127         * This will grant ownership of the path to the web server.
128         */
129        $web_user = trim(`whoami`);
130         
131        if ($debug) {
132            error_log("Running as user: $web_user");
133            error_log("YUI compressor is in $yui");
134        }
135        
136        // For each manifest group, build the corresponding minified, bundled (concatenated) JS and CSS files
137        
138        // Build CSS
139        foreach ($this->_css_includes as $group_name => $group){
140            if ($debug) {
141                error_log("Building CSS for page group '$group_name'...");
142            }
143            $this->buildGroup($yui, $this->_css_path, $this->_css_includes, $group_name, "$group_name.min.css", $debug);
144        }
145        
146        // Build head JS
147        foreach ($this->_js_includes_top as $group_name => $group){
148            if ($debug) {
149                error_log("Building top JS for page group '$group_name'...");
150            }
151            $this->buildGroup($yui, $this->_js_path, $this->_js_includes_top, $group_name, "$group_name-top.min.js", $debug);
152        }
153
154        // Build footer JS
155        foreach ($this->_js_includes_bottom as $group_name => $group){
156            if ($debug) {
157                error_log("Building bottom JS for page group '$group_name'...");
158            }
159            $this->buildGroup($yui, $this->_js_path, $this->_js_includes_bottom, $group_name, "$group_name-bottom.min.js", $debug);
160        }
161    }
162    
163    private function buildGroup($yui, $path_root, $includes, $group_name, $minfile, $debug = false){
164        $paths = $this->mergeIncludes($path_root . "/", $includes, $group_name, "", false, false);
165        
166        // Test permissions on writing to min file:
167        $output_dir = $path_root . "/min/";
168        $output_file = $output_dir . $minfile;
169        $this->write_minified_file($output_dir, $output_file, []);
170
171        // Each file will be minified, and the result appended to the output array
172        $group_arr = [];
173        foreach($paths as $path){
174            if ($debug) {
175                error_log("----Minifying file '$path'");
176            }
177            
178            // This will let us run the compressor and detect any errors
179            $descriptorspec = array(
180                0 => array("pipe", "r"),  // stdin
181                1 => array("pipe", "w"),  // stdout
182                2 => array("pipe", "w"),  // stderr
183            );
184            
185            $process = proc_open("export DYLD_LIBRARY_PATH=''; java -jar $yui $path", $descriptorspec, $pipes);
186            
187            $stdout = stream_get_contents($pipes[1]);
188            fclose($pipes[1]);
189            
190            $stderr = stream_get_contents($pipes[2]);
191            fclose($pipes[2]);
192            
193            // Throw exception if there was an error
194            if ($stderr){
195                throw new \Exception("Unable to minify '$path'.  YUI error message:\n" . $stderr);
196            }
197            
198            // If successful, ppend this minified content to the group file
199            $group_arr[] = $stdout;
200        }
201        
202        // Write minified CSS to the output file
203        if ($debug) {
204            error_log("--Attempting to write to file '$output_file'...");
205        }
206        return $this->write_minified_file($output_dir, $output_file, $group_arr);  
207    }
208        
209    private function write_minified_file($output_dir, $output_file, $content){
210        try {
211            file_put_contents($output_file, implode("\n", $content));
212            return true;
213        } catch (\Exception $e){
214            $web_user = trim(`whoami`);
215            throw new \Exception("Access denied to write to '$output_file'.  Be sure that the directory exists, and that the user '$web_user' has permission to write to this directory.  To grant write access in OSX/Linux/etc, try running the chown command: 'sudo chown -R $web_user:$web_user $output_dir'.");
216        }
217    }    
218    
219}