1<?php
2
3require_once dirname( __FILE__ ).'/Version.php';
4
5/**
6 * Utility for handling the generation and caching of css files
7 *
8 * @package Less
9 * @subpackage cache
10 *
11 */
12class Less_Cache {
13
14	// directory less.php can use for storing data
15	public static $cache_dir	= false;
16
17	// prefix for the storing data
18	public static $prefix		= 'lessphp_';
19
20	// prefix for the storing vars
21	public static $prefix_vars	= 'lessphpvars_';
22
23	// specifies the number of seconds after which data created by less.php will be seen as 'garbage' and potentially cleaned up
24	public static $gc_lifetime	= 604800;
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 Name of the css file
35	 */
36	public static function Get( $less_files, $parser_options = array(), $modify_vars = array() ) {
37		// check $cache_dir
38		if ( isset( $parser_options['cache_dir'] ) ) {
39			Less_Cache::$cache_dir = $parser_options['cache_dir'];
40		}
41
42		if ( empty( Less_Cache::$cache_dir ) ) {
43			throw new Exception( 'cache_dir not set' );
44		}
45
46		if ( isset( $parser_options['prefix'] ) ) {
47			Less_Cache::$prefix = $parser_options['prefix'];
48		}
49
50		if ( empty( Less_Cache::$prefix ) ) {
51			throw new Exception( 'prefix not set' );
52		}
53
54		if ( isset( $parser_options['prefix_vars'] ) ) {
55			Less_Cache::$prefix_vars = $parser_options['prefix_vars'];
56		}
57
58		if ( empty( Less_Cache::$prefix_vars ) ) {
59			throw new Exception( 'prefix_vars not set' );
60		}
61
62		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 = Less_Cache::$cache_dir . Less_Cache::$prefix_vars . sha1( $lessvars ) . '.less';
69
70			if ( !file_exists( $vars_file ) ) {
71				file_put_contents( $vars_file, $lessvars );
72			}
73
74			$less_files += array( $vars_file => '/' );
75		}
76
77		// generate name for compiled css file
78		$hash = md5( json_encode( $less_files ) );
79		$list_file = Less_Cache::$cache_dir . Less_Cache::$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		self::CleanCache();
120
121		return basename( $output_file );
122	}
123
124	/**
125	 * Force the compiler to regenerate the cached css file
126	 *
127	 * @param array $less_files Array of .less files to compile
128	 * @param array $parser_options Array of compiler options
129	 * @param array $modify_vars Array of variables
130	 * @return string Name of the css file
131	 */
132	public static function Regen( $less_files, $parser_options = array(), $modify_vars = array() ) {
133		$parser_options['use_cache'] = false;
134		return self::Get( $less_files, $parser_options, $modify_vars );
135	}
136
137	public static function Cache( &$less_files, $parser_options = array() ) {
138		// get less.php if it exists
139		$file = dirname( __FILE__ ) . '/Less.php';
140		if ( file_exists( $file ) && !class_exists( 'Less_Parser' ) ) {
141			require_once $file;
142		}
143
144		$parser_options['cache_dir'] = Less_Cache::$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 ( strpos( $uri_or_less, "\n" ) !== false ) {
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->allParsedFiles();
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 Less_Cache::$cache_dir.$parser_options['output'];
176		}
177
178		return Less_Cache::$cache_dir.$compiled_name;
179	}
180
181	private static function CompiledName( $files, $extrahash ) {
182		// save the file list
183		$temp = array( Less_Version::cache_version );
184		foreach ( $files as $file ) {
185			$temp[] = filemtime( $file )."\t".filesize( $file )."\t".$file;
186		}
187
188		return Less_Cache::$prefix.sha1( json_encode( $temp ).$extrahash ).'.css';
189	}
190
191	public static function SetCacheDir( $dir ) {
192		Less_Cache::$cache_dir = $dir;
193		self::CheckCacheDir();
194	}
195
196	public static function CheckCacheDir() {
197		Less_Cache::$cache_dir = str_replace( '\\', '/', Less_Cache::$cache_dir );
198		Less_Cache::$cache_dir = rtrim( Less_Cache::$cache_dir, '/' ).'/';
199
200		if ( !file_exists( Less_Cache::$cache_dir ) ) {
201			if ( !mkdir( Less_Cache::$cache_dir ) ) {
202				throw new Less_Exception_Parser( 'Less.php cache directory couldn\'t be created: '.Less_Cache::$cache_dir );
203			}
204
205		} elseif ( !is_dir( Less_Cache::$cache_dir ) ) {
206			throw new Less_Exception_Parser( 'Less.php cache directory doesn\'t exist: '.Less_Cache::$cache_dir );
207
208		} elseif ( !is_writable( Less_Cache::$cache_dir ) ) {
209			throw new Less_Exception_Parser( 'Less.php cache directory isn\'t writable: '.Less_Cache::$cache_dir );
210
211		}
212
213	}
214
215	/**
216	 * Delete unused less.php files
217	 *
218	 */
219	public static function CleanCache() {
220		static $clean = false;
221
222		if ( $clean || empty( Less_Cache::$cache_dir ) ) {
223			return;
224		}
225
226		$clean = true;
227
228		// only remove files with extensions created by less.php
229		// css files removed based on the list files
230		$remove_types = array( 'lesscache' => 1,'list' => 1,'less' => 1,'map' => 1 );
231
232		$files = scandir( Less_Cache::$cache_dir );
233		if ( !$files ) {
234			return;
235		}
236
237		$check_time = time() - self::$gc_lifetime;
238		foreach ( $files as $file ) {
239
240			// don't delete if the file wasn't created with less.php
241			if ( strpos( $file, Less_Cache::$prefix ) !== 0 ) {
242				continue;
243			}
244
245			$parts = explode( '.', $file );
246			$type = array_pop( $parts );
247
248			if ( !isset( $remove_types[$type] ) ) {
249				continue;
250			}
251
252			$full_path = Less_Cache::$cache_dir . $file;
253			$mtime = filemtime( $full_path );
254
255			// don't delete if it's a relatively new file
256			if ( $mtime > $check_time ) {
257				continue;
258			}
259
260			// delete the list file and associated css file
261			if ( $type === 'list' ) {
262				self::ListFiles( $full_path, $list, $css_file_name );
263				if ( $css_file_name ) {
264					$css_file = Less_Cache::$cache_dir . $css_file_name;
265					if ( file_exists( $css_file ) ) {
266						unlink( $css_file );
267					}
268				}
269			}
270
271			unlink( $full_path );
272		}
273
274	}
275
276	/**
277	 * Get the list of less files and generated css file from a list file
278	 *
279	 */
280	static function ListFiles( $list_file, &$list, &$css_file_name ) {
281		$list = explode( "\n", file_get_contents( $list_file ) );
282
283		// pop the cached name that should match $compiled_name
284		$css_file_name = array_pop( $list );
285
286		if ( !preg_match( '/^' . Less_Cache::$prefix . '[a-f0-9]+\.css$/', $css_file_name ) ) {
287			$list[] = $css_file_name;
288			$css_file_name = false;
289		}
290
291	}
292
293}
294