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