<?php
/**
* @version	$Id: cache.php 14331 2011-05-20 10:11:45Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

	defined('FULL_PATH') or die('restricted access!');

	/**
	 * Manager of all implemented caching handlers
	 *
	 */
	class kCache extends kBase {

		/**
		 * Object of cache handler
		 *
		 * @var FakeCacheHandler
		 */
		var $_handler = null;

		/**
		 * Part of what we retrieve will be stored locally (per script run) not to bother memcache a lot
		 *
		 * @var Array
		 */
		var $_localStorage = Array ();

		/**
		 * What type of caching is being used
		 *
		 * @var int
		 */
		var $cachingType = CACHING_TYPE_NONE;

		/**
		 * Cache usage statistics (per script run)
		 *
		 * @var Array
		 */
		var $statistics = Array ();

		/**
		 * Debug cache usage
		 *
		 * @var bool
		 */
		var $debugCache = false;

		/**
		 * Displays cache usage statistics
		 *
		 * @var bool
		 */
		var $displayCacheStatistics = false;

		/**
		 * Site key name
		 * Prepended to each cached key name
		 *
		 * @var string
		 */
		var $siteKeyName = '';

		/**
		 * Site key value
		 * Prepended to each cached key name
		 *
		 * @var string
		 */
		var $siteKeyValue = null;

		function kCache()
		{
			parent::kBase();

			$this->siteKeyName = 'site_serial:' . crc32(SQL_TYPE . '://' . SQL_USER . ':' . SQL_PASS . '@' . SQL_SERVER . ':' . TABLE_PREFIX . ':' . SQL_DB);

			// get cache handler class to use
			if (array_key_exists('CacheHandler', $GLOBALS['vars']) && $GLOBALS['vars']['CacheHandler']) {
				// for advanced users, who want to save one SQL on each page load
				$handler_class = $GLOBALS['vars']['CacheHandler'] . 'CacheHandler';
			}
			else {
				$handler_class = $this->Application->ConfigValue('CacheHandler') . 'CacheHandler';
			}

			// defined cache handler doen't exist -> use default
			if (!class_exists($handler_class)) {
				$handler_class = 'FakeCacheHandler';
			}

			$handler = new $handler_class();

			if (!$handler->isWorking()) {
				// defined cache handler is not working -> use default
				trigger_error('Failed to initialize "<strong>' . $handler_class . '</strong>" caching handler.', E_USER_WARNING);

				$handler = new FakeCacheHandler();
			}
			elseif ($this->Application->isDebugMode() && ($handler->cachingType == CACHING_TYPE_MEMORY)) {
				$this->Application->Debugger->appendHTML('Memory Caching: "<strong>' . $handler_class . '</strong>"');
			}

			$this->_handler =& $handler;
			$this->cachingType = $handler->cachingType;
			$this->debugCache = $handler->cachingType == CACHING_TYPE_MEMORY && $this->Application->isDebugMode();
			$this->displayCacheStatistics = defined('DBG_CACHE') && DBG_CACHE && $this->Application->isDebugMode();
		}

		/**
		 * Returns caching type of current storage engine
		 *
		 * @return int
		 */
		function getCachingType()
		{
			return $this->cachingType;
		}


		/**
		 * Stores value to cache
		 *
		 * @param string $name
		 * @param mixed $value
		 * @param int $expires cache record expiration time in seconds
		 */
		function setCache($name, $value, $expiration)
		{
			$name = $this->prepareKeyName($name);
			$this->_localStorage[$name] = $value;

			return $this->_handler->set($name, $value, $expiration);
		}

		/**
		 * Returns value from cache
		 *
		 * @param string $name
		 * @param bool $store_locally store data locally after retrieved
		 * @param bool $replace_serials
		 * @return mixed
		 */
		function getCache($name, $store_locally = true, $replace_serials = true)
		{
			$name = $this->prepareKeyName($name, $replace_serials);

			if ($store_locally) {
				if (array_key_exists($name, $this->_localStorage)) {
					if ($this->displayCacheStatistics) {
						$this->setStatistics($name, $this->_localStorage[$name]);
					}

					return $this->_localStorage[$name];
				}
			}

			$res = $this->_handler->get($name);

			if ($replace_serials && $this->debugCache) {
				// don't display subsequent serial cache retrievals (ones, that are part of keys)
				if (is_array($res)) {
					$this->Application->Debugger->appendHTML('Restoring key "' . $name . '". Type: ' . gettype($res) . '.');
				}
				else {
					$res_display = strip_tags($res);

					if (strlen($res_display) > 200) {
						$res_display = substr($res_display, 0, 50) . ' ...';
					}

					$this->Application->Debugger->appendHTML('Restoring key "' . $name . '" resulted [' . $res_display . ']');
				}
			}

			if ($store_locally && ($res !== false)) {
				$this->_localStorage[$name] = $res;

				if ($this->displayCacheStatistics) {
					$this->setStatistics($name, $res);
				}
			}

			return $res;
		}

		/**
		 * Deletes value from cache
		 *
		 * @param string $name
		 * @return mixed
		 */
		function delete($name)
		{
			$name = $this->prepareKeyName($name);
			unset($this->_localStorage[$name]);

			return $this->_handler->delete($name);
		}

		/**
		 * Reset's all memory cache at once
		 */
		function reset()
		{
			// don't check for enabled, because we maybe need to reset cache anyway
			if ($this->cachingType == CACHING_TYPE_TEMPORARY) {
				return ;
			}

			$site_key = $this->_cachePrefix(true);

			$this->_handler->set($site_key, $this->_handler->get($site_key) + 1);
		}

		/**
		 * Replaces serials and adds unique site prefix to cache variable name
		 *
		 * @param string $name
		 * @param bool $replace_serials
		 * @return string
		 */
		function prepareKeyName($name, $replace_serials = true)
		{
			// replace serials in key name
			if ($replace_serials && preg_match_all('/\[%(.*?)%\]/', $name, $regs)) {
				// [%LangSerial%] - prefix-wide serial in case of any change in "lang" prefix
				// [%LangIDSerial:5%] - one id-wide serial in case of data, associated with given id was changed
				// [%CiIDSerial:ItemResourceId:5%] - foreign key-based serial in case of data, associated with given foreign key was changed
				foreach ($regs[1] as $serial_name) {
					$name = str_replace('[%' . $serial_name . '%]', '[' . $serial_name . '=' . $this->getCache($serial_name, true, false) . ']', $name);
				}
			}

			if ($this->cachingType == CACHING_TYPE_TEMPORARY) {
				return $name;
			}

			// add site-wide prefix to key
			return $this->_cachePrefix() . $name;
		}

		/**
		 * Returns site-wide caching prefix
		 *
		 * @param bool $only_site_key_name
		 * @return string
		 */
		function _cachePrefix($only_site_key_name = false)
		{
			if ($only_site_key_name) {
				return $this->siteKeyName;
			}

			if ( !isset($this->siteKeyValue) ) {
				$this->siteKeyValue = $this->_handler->get($this->siteKeyName);

				if (!$this->siteKeyValue) {
					$this->siteKeyValue = 1;
					$this->_handler->set($this->siteKeyName, $this->siteKeyValue);
				}
			}

			return "{$this->siteKeyName}:{$this->siteKeyValue}:";
		}

		function setStatistics($name, $found)
		{
			if (strpos($name, ']:') !== false) {
				list ($cache_name, $name) = explode(']:', $name, 2);
			}
			else {
				$cache_name = '-';
			}

			if (!array_key_exists($cache_name, $this->statistics)) {
				$this->statistics[$cache_name] = Array ();
			}

			if (!array_key_exists($name, $this->statistics[$cache_name])) {
				$this->statistics[$cache_name][$name] = Array ();
			}

			$status_key = $found ? 'found' : 'not_found';

			if (!isset($this->statistics[$cache_name][$name][$status_key])) {
				$this->statistics[$cache_name][$name][$status_key] = 0;
			}

			$this->statistics[$cache_name][$name][$status_key]++;
		}

		/**
		 * Returns storage size in bytes
		 *
		 * @return int
		 */
		function getStorageSize()
		{
			return strlen( serialize($this->_localStorage) );
		}

		function printStatistics()
		{
			$cache_size = $this->getStorageSize();

			$this->Application->Debugger->appendHTML('<strong>Cache Size:</strong> ' . formatSize($cache_size) . ' (' . $cache_size . ')');

			foreach ($this->statistics as $cache_name => $cache_data) {
				foreach ($cache_data as $key => $value) {
					if (!array_key_exists('found', $value) || $value['found'] == 1) {
						// remove cached records, that were used only 1 or 2 times
						unset($this->statistics[$cache_name][$key]);
					}
				}
			}

			print_pre($this->statistics, 'Cache Statistics:');
		}
	}


	class FakeCacheHandler {

		var $cachingType = CACHING_TYPE_TEMPORARY;

		function FakeCacheHandler()
		{

		}

		/**
		 * Retrieves value from cache
		 *
		 * @param string $name
		 * @return mixed
		 */
		function get($name)
		{
			return false;
		}

		/**
		 * Stores value in cache
		 *
		 * @param string $name
		 * @param mixed $value
		 * @param int $expiration
		 * @return bool
		 */
		function set($name, $value, $expiration = 0)
		{
			return true;
		}

		/**
		 * Deletes key from cach
		 *
		 * @param string $name
		 * @return bool
		 */
		function delete($name)
		{
			return true;
		}

		/**
		 * Determines, that cache storage is working fine
		 *
		 * @return bool
		 */
		function isWorking()
		{
			return true;
		}
	}


	class MemcacheCacheHandler {

		var $_enabled = false;

		/**
		 * Memcache connection
		 *
		 * @var Memcache
		 */
		var $_handler = null;

		var $cachingType = CACHING_TYPE_MEMORY;

		function MemcacheCacheHandler()
		{
			if (array_key_exists('MemcacheServers', $GLOBALS['vars'])) {
				// for advanced users, who want to save one SQL on each page load
				$memcached_servers = $GLOBALS['vars']['MemcacheServers'];
			}
			else {
				$application =& kApplication::Instance();
				$memcached_servers = $application->ConfigValue('MemcacheServers');
			}

			if ($memcached_servers && class_exists('Memcache')) {
				$this->_enabled = true;
				$this->_handler = new Memcache();
				$servers = explode(';', $memcached_servers);

			 	foreach ($servers as $server) {
			 		if ( preg_match('/(.*):([\d]+)$/', $server, $regs) ) {
			 			// "hostname:port" OR "unix:///path/to/socket:0"
			 			$server = $regs[1];
			 			$port = $regs[2];
			 		}
			 		else {
			 			$port = 11211;
			 		}

			 		$this->_handler->addServer($server, $port);
			 	}

			 	// verify, that memcache server is working
			 	if (!$this->_handler->set('test', 1)) {
					$this->_enabled = false;
				}
			}
		}

		/**
		 * Retrieves value from cache
		 *
		 * @param string $name
		 * @return mixed
		 */
		function get($name)
		{
			return $this->_handler->get($name);
		}

		/**
		 * Stores value in cache
		 *
		 * @param string $name
		 * @param mixed $value
		 * @param int $expiration
		 * @return bool
		 */
		function set($name, $value, $expiration = 0)
		{
			// 0 - don't use compression
			return $this->_handler->set($name, $value, 0, $expiration);
		}

		/**
		 * Deletes key from cache
		 *
		 * @param string $name
		 * @return bool
		 */
		function delete($name)
		{
			return $this->_handler->delete($name, 0);
		}

		/**
		 * Determines, that cache storage is working fine
		 *
		 * @return bool
		 */
		function isWorking()
		{
			return $this->_enabled;
		}
	}


	class ApcCacheHandler {

		var $_enabled = false;

		var $cachingType = CACHING_TYPE_MEMORY;

		function ApcCacheHandler()
		{
			$this->_enabled = function_exists('apc_fetch');

		 	// verify, that apc is working
		 	if ($this->_enabled && !$this->set('test', 1)) {
				$this->_enabled = false;
			}
		}

		/**
		 * Retrieves value from cache
		 *
		 * @param string $name
		 * @return mixed
		 */
		function get($name)
		{
			return apc_fetch($name);
		}

		/**
		 * Stores value in cache
		 *
		 * @param string $name
		 * @param mixed $value
		 * @param int $expiration
		 * @return bool
		 */
		function set($name, $value, $expiration = 0)
		{
			return apc_store($name, $value, $expiration);
		}

		/**
		 * Deletes key from cache
		 *
		 * @param string $name
		 * @return bool
		 */
		function delete($name)
		{
			return apc_delete($name);
		}

		/**
		 * Determines, that cache storage is working fine
		 *
		 * @return bool
		 */
		function isWorking()
		{
			return $this->_enabled;
		}
	}

	class XCacheCacheHandler {

		var $_enabled = false;

		var $cachingType = CACHING_TYPE_MEMORY;

		function XCacheCacheHandler()
		{
			$this->_enabled = function_exists('xcache_get');

		 	// verify, that xcache is working
		 	if ($this->_enabled && !$this->set('test', 1)) {
				$this->_enabled = false;
			}
		}

		/**
		 * Retrieves value from cache
		 *
		 * @param string $name
		 * @return mixed
		 */
		function get($name)
		{
			return xcache_isset($name) ? xcache_get($name) : false;
		}

		/**
		 * Stores value in cache
		 *
		 * @param string $name
		 * @param mixed $value
		 * @param int $expiration
		 * @return bool
		 */
		function set($name, $value, $expiration = 0)
		{
			return xcache_set($name, $value, $expiration);
		}

		/**
		 * Deletes key from cache
		 *
		 * @param string $name
		 * @return bool
		 */
		function delete($name)
		{
			return xcache_unset($name);
		}

		/**
		 * Determines, that cache storage is working fine
		 *
		 * @return bool
		 */
		function isWorking()
		{
			return $this->_enabled;
		}
	}