Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.03% covered (warning)
71.03%
76 / 107
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Less_Cache
71.03% covered (warning)
71.03%
76 / 107
33.33% covered (danger)
33.33%
3 / 9
94.24
0.00% covered (danger)
0.00%
0 / 1
 Get
74.42% covered (warning)
74.42%
32 / 43
0.00% covered (danger)
0.00%
0 / 1
21.84
 Regen
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 Cache
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
3.07
 OutputFile
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 CompiledName
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 SetCacheDir
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 CheckCacheDir
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
7.19
 CleanCache
66.67% covered (warning)
66.67%
18 / 27
0.00% covered (danger)
0.00%
0 / 1
15.48
 ListFiles
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
2.26
1<?php
2
3/**
4 * Utility for handling the generation and caching of css files
5 */
6class Less_Cache {
7
8    /** @var string|false Directory less.php can use for storing data */
9    public static $cache_dir = false;
10
11    /** @var string Prefix for the storing data */
12    public static $prefix = 'lessphp_';
13
14    /** @var string Prefix for the storing vars */
15    public static $prefix_vars = 'lessphpvars_';
16
17    /**
18     * @var int Specifies the number of seconds after which data created by less.php will be seen
19     *  as 'garbage' and potentially cleaned up
20     */
21    public static $gc_lifetime = 604800;
22
23    /**
24     * Save and reuse the results of compiled less files.
25     * The first call to Get() will generate css and save it.
26     * Subsequent calls to Get() with the same arguments will return the same css filename
27     *
28     * @param array $less_files Array of .less files to compile
29     * @param array $parser_options Array of compiler options
30     * @param array $modify_vars Array of variables
31     * @return string|false Name of the css file
32     */
33    public static function Get( $less_files, $parser_options = [], $modify_vars = [] ) {
34        // check $cache_dir
35        if ( isset( $parser_options['cache_dir'] ) ) {
36            self::$cache_dir = $parser_options['cache_dir'];
37        }
38
39        if ( empty( self::$cache_dir ) ) {
40            throw new Exception( 'cache_dir not set' );
41        }
42
43        if ( isset( $parser_options['prefix'] ) ) {
44            self::$prefix = $parser_options['prefix'];
45        }
46
47        if ( empty( self::$prefix ) ) {
48            throw new Exception( 'prefix not set' );
49        }
50
51        if ( isset( $parser_options['prefix_vars'] ) ) {
52            self::$prefix_vars = $parser_options['prefix_vars'];
53        }
54
55        if ( empty( self::$prefix_vars ) ) {
56            throw new Exception( 'prefix_vars not set' );
57        }
58
59        self::CheckCacheDir();
60        $less_files = (array)$less_files;
61
62        // create a file for variables
63        if ( !empty( $modify_vars ) ) {
64            $lessvars = Less_Parser::serializeVars( $modify_vars );
65            $vars_file = self::$cache_dir . self::$prefix_vars . sha1( $lessvars ) . '.less';
66
67            if ( !file_exists( $vars_file ) ) {
68                file_put_contents( $vars_file, $lessvars );
69            }
70
71            $less_files += [ $vars_file => '/' ];
72        }
73
74        // generate name for compiled css file
75        $hash = md5( json_encode( $less_files ) );
76        $list_file = self::$cache_dir . self::$prefix . $hash . '.list';
77
78        // check cached content
79        if ( !isset( $parser_options['use_cache'] ) || $parser_options['use_cache'] === true ) {
80            if ( file_exists( $list_file ) ) {
81
82                self::ListFiles( $list_file, $list, $cached_name );
83                $compiled_name = self::CompiledName( $list, $hash );
84
85                // if $cached_name is the same as the $compiled name, don't regenerate
86                if ( !$cached_name || $cached_name === $compiled_name ) {
87
88                    $output_file = self::OutputFile( $compiled_name, $parser_options );
89
90                    if ( $output_file && file_exists( $output_file ) ) {
91                        @touch( $list_file );
92                        return basename( $output_file ); // for backwards compatibility, we just return the name of the file
93                    }
94                }
95            }
96        }
97
98        $compiled = self::Cache( $less_files, $parser_options );
99        if ( !$compiled ) {
100            return false;
101        }
102
103        $compiled_name = self::CompiledName( $less_files, $hash );
104        $output_file = self::OutputFile( $compiled_name, $parser_options );
105
106        // save the file list
107        $list = $less_files;
108        $list[] = $compiled_name;
109        $cache = implode( "\n", $list );
110        file_put_contents( $list_file, $cache );
111
112        // save the css
113        file_put_contents( $output_file, $compiled );
114
115        // clean up
116        self::CleanCache();
117
118        return basename( $output_file );
119    }
120
121    /**
122     * Force the compiler to regenerate the cached css file
123     *
124     * @param array $less_files Array of .less files to compile
125     * @param array $parser_options Array of compiler options
126     * @param array $modify_vars Array of variables
127     * @return string Name of the css file
128     */
129    public static function Regen( $less_files, $parser_options = [], $modify_vars = [] ) {
130        $parser_options['use_cache'] = false;
131        return self::Get( $less_files, $parser_options, $modify_vars );
132    }
133
134    public static function Cache( &$less_files, $parser_options = [] ) {
135        $parser_options['cache_dir'] = self::$cache_dir;
136        $parser = new Less_Parser( $parser_options );
137
138        // combine files
139        foreach ( $less_files as $file_path => $uri_or_less ) {
140
141            // treat as less markup if there are newline characters
142            if ( strpos( $uri_or_less, "\n" ) !== false ) {
143                $parser->Parse( $uri_or_less );
144                continue;
145            }
146
147            $parser->ParseFile( $file_path, $uri_or_less );
148        }
149
150        $compiled = $parser->getCss();
151
152        $less_files = $parser->getParsedFiles();
153
154        return $compiled;
155    }
156
157    private static function OutputFile( $compiled_name, $parser_options ) {
158        // custom output file
159        if ( !empty( $parser_options['output'] ) ) {
160
161            // relative to cache directory?
162            if ( preg_match( '#[\\\\/]#', $parser_options['output'] ) ) {
163                return $parser_options['output'];
164            }
165
166            return self::$cache_dir . $parser_options['output'];
167        }
168
169        return self::$cache_dir . $compiled_name;
170    }
171
172    private static function CompiledName( $files, $extrahash ) {
173        // save the file list
174        $temp = [ Less_Version::cache_version ];
175        foreach ( $files as $file ) {
176            $temp[] = filemtime( $file ) . "\t" . filesize( $file ) . "\t" . $file;
177        }
178
179        return self::$prefix . sha1( json_encode( $temp ) . $extrahash ) . '.css';
180    }
181
182    public static function SetCacheDir( $dir ) {
183        self::$cache_dir = $dir;
184        self::CheckCacheDir();
185    }
186
187    public static function CheckCacheDir() {
188        self::$cache_dir = str_replace( '\\', '/', self::$cache_dir );
189        self::$cache_dir = rtrim( self::$cache_dir, '/' ) . '/';
190
191        if ( !file_exists( self::$cache_dir ) ) {
192            if ( !mkdir( self::$cache_dir ) ) {
193                throw new Less_Exception_Parser( 'Less.php cache directory couldn\'t be created: ' . self::$cache_dir );
194            }
195
196        } elseif ( !is_dir( self::$cache_dir ) ) {
197            throw new Less_Exception_Parser( 'Less.php cache directory doesn\'t exist: ' . self::$cache_dir );
198
199        } elseif ( !is_writable( self::$cache_dir ) ) {
200            throw new Less_Exception_Parser( 'Less.php cache directory isn\'t writable: ' . self::$cache_dir );
201
202        }
203    }
204
205    /**
206     * Delete unused less.php files
207     */
208    public static function CleanCache() {
209        static $clean = false;
210
211        if ( $clean || empty( self::$cache_dir ) ) {
212            return;
213        }
214
215        $clean = true;
216
217        // only remove files with extensions created by less.php
218        // css files removed based on the list files
219        $remove_types = [ 'lesscache' => 1, 'list' => 1, 'less' => 1, 'map' => 1 ];
220
221        $files = scandir( self::$cache_dir );
222        if ( !$files ) {
223            return;
224        }
225
226        $check_time = time() - self::$gc_lifetime;
227        foreach ( $files as $file ) {
228
229            // don't delete if the file wasn't created with less.php
230            if ( strpos( $file, self::$prefix ) !== 0 ) {
231                continue;
232            }
233
234            $parts = explode( '.', $file );
235            $type = array_pop( $parts );
236
237            if ( !isset( $remove_types[$type] ) ) {
238                continue;
239            }
240
241            $fullPath = self::$cache_dir . $file;
242            $mtime = filemtime( $fullPath );
243
244            // don't delete if it's a relatively new file
245            if ( $mtime > $check_time ) {
246                continue;
247            }
248
249            // delete the list file and associated css file
250            if ( $type === 'list' ) {
251                self::ListFiles( $fullPath, $list, $css_file_name );
252                if ( $css_file_name ) {
253                    $css_file = self::$cache_dir . $css_file_name;
254                    if ( file_exists( $css_file ) ) {
255                        unlink( $css_file );
256                    }
257                }
258            }
259
260            unlink( $fullPath );
261        }
262    }
263
264    /**
265     * Get the list of less files and generated css file from a list file
266     */
267    public static function ListFiles( $list_file, &$list, &$css_file_name ) {
268        $list = explode( "\n", file_get_contents( $list_file ) );
269
270        // pop the cached name that should match $compiled_name
271        $css_file_name = array_pop( $list );
272
273        if ( !preg_match( '/^' . self::$prefix . '[a-f0-9]+\.css$/', $css_file_name ) ) {
274            $list[] = $css_file_name;
275            $css_file_name = false;
276        }
277    }
278
279}