1<?php
2/*
3 * e107 website system
4 *
5 * Copyright (C) 2008-2010 e107 Inc (e107.org)
6 * Released under the terms and conditions of the
7 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
8 *
9 * Cache handler
10 *
11 * $URL$
12 * $Id$
13*/
14
15if (!defined('e107_INIT')) { exit; }
16
17define('CACHE_PREFIX','<?php exit;');
18
19/**
20 * Class to cache data as files, improving site speed and throughput.
21 * FIXME - pref independant cache handler, cache drivers
22 *
23 * @package     e107
24 * @subpackage	e107_handlers
25 * @version     $Id$
26 * @author      e107 Inc
27 */
28class ecache {
29
30	public $CachePageMD5;
31	public $CachenqMD5;
32	public $UserCacheActive;			// Checkable flag - TRUE if user cache enabled
33	public $SystemCacheActive;			// Checkable flag - TRUE if system cache enabled
34
35	const CACHE_PREFIX = '<?php exit;';
36
37	function __construct()
38	{
39		//$this->UserCacheActive = e107::getPref('cachestatus');
40		//$this->SystemCacheActive = e107::getPref('syscachestatus');
41	}
42
43	/**
44	 * Set the MD5 Hash
45	 */
46	public function setMD5($text, $hash=true)
47	{
48		if($text === null)
49		{
50			$this->CachePageMD5 = md5(e_BASE.e_LANGUAGE.THEME.USERCLASS_LIST.defset('e_QUERY').filemtime(THEME.'theme.php'));
51			return $this;
52		}
53
54		$this->CachePageMD5 = ($hash === true) ? md5($text) : $text;
55		return $this;
56	}
57
58
59	public function getMD5()
60	{
61		return $this->CachePageMD5;
62	}
63
64	/**
65	* @return string
66	* @param string $query
67	* @desc Internal class function that returns the filename of a cache file based on the query.
68	* @scope private
69	* If the tag begins 'menu_', e_QUERY is not included in the hash which creates the file name
70	*/
71	function cache_fname($CacheTag, $syscache = false)
72	{
73		if(strpos($CacheTag, "nomd5_") === 0) {
74			// Add 'nomd5' to indicate we are not calculating an md5
75			$CheckTag = '_nomd5';
76		}
77		elseif (isset($this) && $this instanceof ecache)
78		{
79			if (defined("THEME"))
80			{
81				if (strpos($CacheTag, "nq_") === 0)
82				{
83					// We do not care about e_QUERY, so don't use it in the md5 calculation
84					if (!$this->CachenqMD5)
85					{
86						$this->CachenqMD5 = md5(e_BASE.(defined("ADMIN") && ADMIN == true ? "admin" : "").e_LANGUAGE.THEME.USERCLASS_LIST.filemtime(THEME.'theme.php'));
87					}
88					// Add 'nq' to indicate we are not using e_QUERY
89					$CheckTag = '_nq_'.$this->CachenqMD5;
90
91				}
92				else
93				{
94					// It's a page - need the query in the hash
95					if (!$this->CachePageMD5)
96					{
97						$this->CachePageMD5 = md5(e_BASE.e_LANGUAGE.THEME.USERCLASS_LIST.defset('e_QUERY').filemtime(THEME.'theme.php'));
98					}
99					$CheckTag = '_'.$this->CachePageMD5;
100				}
101			}
102			else
103			{
104				// Check if a custom CachePageMD5 is in use in e_module.php.
105				$CheckTag = ($this->CachePageMD5) ? "_".$this->CachePageMD5 : "";
106			}
107		}
108		else
109		{
110			$CheckTag = '';
111		}
112		$q = ($syscache ? "S_" : "C_").preg_replace("#\W#", "_", $CacheTag);
113
114		if($syscache === true)
115		{
116			$CheckTag = ''; // no MD5 on system cache. XXX To be Checked.
117		}
118
119		$fname = e_CACHE_CONTENT.$q.$CheckTag.'.cache.php';
120		//echo "cache f_name = $fname <br />";
121		return $fname;
122	}
123
124	/**
125	 * Retrieve Cache data.
126	 * @param $CacheTag
127	 * @param bool|int $MaximumAge the time in minutes before the cache file 'expires'
128	 * @param bool $ForcedCheck check even if cache pref is disabled.
129	 * @param bool $syscache set to true when checking sys cache.
130	 * @return string
131	 * @desc Returns the data from the cache file associated with $query, else it returns false if there is no cache for $query.
132	 * @scope public
133	 */
134	public function retrieve($CacheTag, $MaximumAge = false, $ForcedCheck = false, $syscache = false)
135	{
136		if(($ForcedCheck != false ) || ($syscache == false && $this->UserCacheActive) || ($syscache == true && $this->SystemCacheActive) && !e107::getParser()->checkHighlighting())
137		{
138			$cache_file = (isset($this) && $this instanceof ecache ? $this->cache_fname($CacheTag, $syscache) : self::cache_fname($CacheTag, $syscache));
139
140			if(file_exists($cache_file))
141			{
142				if ($MaximumAge !== false && (filemtime($cache_file) + ($MaximumAge * 60)) < time()) {
143					unlink($cache_file);
144					return false;
145				}
146				else
147				{
148					$ret = file_get_contents($cache_file);
149					if (substr($ret,0,strlen(self::CACHE_PREFIX)) == self::CACHE_PREFIX)
150					{
151						$ret = substr($ret, strlen(self::CACHE_PREFIX));
152					}
153					elseif(substr($ret,0,5) == '<?php')
154					{
155						$ret = substr($ret, 5);		// Handle the history for now
156					}
157					return $ret;
158				}
159			}
160			else
161			{
162			//	e107::getDebug()->log("Couldn't find cache file: ".json_encode($cache_file));
163				return false;
164			}
165		}
166		return false;
167	}
168
169	/**
170	* @return string
171	* @param string $CacheTag
172	* @param int $MaximumAge the time in minutes before the cache file 'expires'
173	 * @param boolean $force
174	* @desc Returns the data from the cache file associated with $query, else it returns false if there is no cache for $query.
175	* @scope public
176	*/
177	function retrieve_sys($CacheTag, $MaximumAge = false, $ForcedCheck = false)
178	{
179		if(isset($this) && $this instanceof ecache)
180		{
181			return $this->retrieve($CacheTag, $MaximumAge, $ForcedCheck, true);
182		}
183		else
184		{
185			return self::retrieve($CacheTag, $MaximumAge, $ForcedCheck, true);
186		}
187	}
188
189
190	/**
191	 *
192	 * @param string $CacheTag - name of tag for future retrieval - should NOT contain an MD5.
193	 * @param string $Data - data to be cached
194	 * @param boolean $ForceCache [optional] if TRUE, writes cache even when disabled in admin prefs.
195	 * @param boolean $bRaw [optional] if TRUE, writes data exactly as provided instead of prefacing with php leadin
196	 * @param boolean $syscache [optional]
197	 * @return none
198	 */
199	public function set($CacheTag, $Data, $ForceCache = false, $bRaw=0, $syscache = false)
200	{
201		if(defined('E107_INSTALL') && E107_INSTALL === true)
202		{
203			return null;
204		}
205
206		if(($ForceCache != false ) || ($syscache == false && $this->UserCacheActive) || ($syscache == true && $this->SystemCacheActive) && !e107::getParser()->checkHighlighting())
207		{
208			$cache_file = (isset($this) && $this instanceof ecache ? $this->cache_fname($CacheTag, $syscache) : self::cache_fname($CacheTag, $syscache));
209			@file_put_contents($cache_file, ($bRaw? $Data : self::CACHE_PREFIX.$Data) );
210			@chmod($cache_file, 0755); //Cache should not be world-writeable
211			@touch($cache_file);
212		}
213	}
214
215	/**
216	* @return void
217	* @param string $CacheTag - name of tag for future retrieval - should NOT contain an MD5
218	* @param string $Data - data to be cached
219	* @param bool   $ForceCache (optional, default false) - if TRUE, writes cache even when disabled
220	* @param bool   $bRaw (optional, default false) - if TRUE, writes data exactly as provided instead of prefacing with php leadin
221	* @desc Creates / overwrites the cache file for $query, $text is the data to store for $query.
222	* @scope public
223	*/
224	function set_sys($CacheTag, $Data, $ForceCache = false, $bRaw=0)
225	{
226		if(isset($this) && $this instanceof ecache)
227		{
228			return $this->set($CacheTag, $Data, $ForceCache, $bRaw, true);
229		}
230		else
231		{
232			self::set($CacheTag, $Data, $ForceCache, $bRaw, true);
233		}
234	}
235
236
237	/**
238	 * Deletes cache files. If $query is set, deletes files named {$CacheTag}*.cache.php, if not it deletes all cache files - (*.cache.php)
239	 *
240	 * @param string $CacheTag
241	 * @param boolean $syscache
242	 * @param boolean $related clear also 'nq_' and 'nomd5_' entries
243	 * @return bool
244	 *
245	 */
246	public function clear($CacheTag = '', $syscache = false, $related = false)
247	{
248
249		$file = ($CacheTag) ? preg_replace("#\W#", "_", $CacheTag)."*.cache.php" : "*.cache.php";
250		e107::getEvent()->triggerAdminEvent('cache_clear', "cachetag=$CacheTag&file=$file&syscache=$syscache");
251		$ret = self::delete(e_CACHE_CONTENT, $file, $syscache);
252
253		if($CacheTag && $related) //TODO - too dirty - add it to the $file pattern above
254		{
255			self::delete(e_CACHE_CONTENT, 'nq_'.$file, $syscache);
256			self::delete(e_CACHE_CONTENT, 'nomd5_'.$file, $syscache);
257			//ecache::delete(e_CACHE_CONTENT, 'nq_'.$file, $syscache);
258			//ecache::delete(e_CACHE_CONTENT, 'nomd5_'.$file, $syscache);
259		}
260		return $ret;
261	}
262
263	/**
264	* @return bool
265	* @param string $CacheTag
266	* @desc Deletes cache files. If $query is set, deletes files named {$CacheTag}*.cache.php, if not it deletes all cache files - (*.cache.php)
267	*/
268	function clear_sys($CacheTag = '', $related = false)
269	{
270
271
272		if(isset($this) && $this instanceof ecache)
273		{
274			return $this->clear($CacheTag, true, $related);
275		}
276		else
277		{
278			self::clear($CacheTag, true, $related);
279		//	ecache::clear($CacheTag, true, $related);
280		}
281	}
282
283	/**
284	* @return bool
285	* @param string $dir
286	* @param string $pattern
287	* @desc Internal class function to allow deletion of cache files using a pattern, default '*.*'
288	* @scope private
289	*/
290	function delete($dir, $pattern = "*.*", $syscache = false) {
291		$deleted = false;
292		$pattern = ($syscache ? "S_" : "C_").$pattern;
293		$pattern = str_replace(array("\*", "\?"), array(".*", "."), preg_quote($pattern));
294		if (substr($dir, -1) != "/") {
295			$dir .= "/";
296		}
297		if (is_dir($dir))
298		{
299 			$d = opendir($dir);
300			while ($file = readdir($d)) {
301				if (is_file($dir.$file) && preg_match("/^{$pattern}$/", $file)) {
302					if (unlink($dir.$file)) {
303						$deleted[] = $file;
304					}
305				}
306			}
307			closedir($d);
308			return true;
309		} else {
310			return false;
311		}
312	}
313
314
315	/**
316	 * Clear Full Cache
317	 * @param string $type: content | system| browser | db | image | js | css | library
318	 * @example clearAll('db');
319	 */
320
321	function clearAll($type,$mask = null)
322	{
323		$path = null;
324
325		if($type =='content')
326		{
327			$this->clear();
328			return;
329		}
330
331		if($type === 'system')
332		{
333			$this->clear_sys();
334			return;
335		}
336
337		if($type === 'browser')
338		{
339			e107::getConfig()->set('e_jslib_browser_cache', time())->save(false);
340			return;
341		}
342
343		if($type === 'db')
344		{
345			$path = e_CACHE_DB;
346			$mask = ($mask == null) ? '.*\.php' : $mask;
347		}
348
349		if($type === 'image')
350		{
351			$path = e_CACHE_IMAGE;
352			$mask = ($mask == null) ? '.*(\.cache\.bin|\.jpg|\.jpeg|\.png|\.gif)' : $mask;
353		}
354
355		if($type === 'js')
356		{
357			$path = e_WEB."cache/";
358			$mask = ($mask == null) ? '.*\.js' : $mask;
359		}
360
361		if($type === 'css')
362		{
363			$path = e_WEB."cache/";
364			$mask = ($mask == null) ? '.*\.css' : $mask;
365		}
366
367		if($type === 'library')
368		{
369			$path = e_CACHE_CONTENT;
370			$mask = ($mask == null) ? 'S_Library_.*\.cache\.php' : $mask;
371		}
372
373		if((null == $path) || (null == $mask))
374		{
375			return;
376		}
377
378		$fl = e107::getFile(false);
379		$fl->mode = 'fname';
380		$files = $fl->get_files($path, $mask);
381
382		if($files)
383		{
384			foreach ($files as $file)
385			{
386				unlink($path.$file);
387			}
388		}
389	}
390
391}
392