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