<?php
/*
 * $RCSfile: GalleryDataCache.class,v $
 *
 * Gallery - a web based photo album viewer and editor
 * Copyright (C) 2000-2005 Bharat Mediratta
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA  02110-1301, USA.
 */
/**
 * @version $Revision: 1.36.2.1 $ $Date: 2005/10/13 17:37:50 $
 * @package GalleryCore
 * @author Bharat Mediratta <bharat@menalto.com>
 */

/**
 * Utility class for caching data
 *
 * Very useful in the case where retrieving or building data sets is expensive,
 * and the data doesn't change during the lifetime of the request.  This class
 * serves as a hash table where any data class can store and retrieve cached
 * data.
 *
 * @package GalleryCore
 * @subpackage Classes
 */
class GalleryDataCache {

    /**
     * Get the static cache
     *
     * @return array the cache
     * @staticvar cache the singleton cache
     * @static
     * @access private
     */
    function &_getCache() {
	static $cache;
	if (!isset($cache)) {
	    $cache['maxKeys'] = 300;
	    $cache['timestamps'] = array();
	    $cache['keys'] = array();
	    $cache['protected'] = array();
	}
	return $cache;
    }

    /**
     * Is in-memory caching enabled?
     * @return bool true if it's enabled
     */
    function &isMemoryCachingEnabled() {
	static $enabled;
	if (!isset($enabled)) {
	    $enabled = true;
	}
	return $enabled;
    }

    /**
     * Turn in-memory caching on or off
     * @param bool true or false
     */
    function setMemoryCachingEnabled($bool) {
	$enabled =& GalleryDataCache::isMemoryCachingEnabled();
	$enabled = $bool;
    }

    /**
     * Store data in the cache
     *
     * You must provide a unique key.  Existing keys are overwritten.
     *
     * @param string the key
     * @param mixed the data
     * @param boolean should this key survive a reset call?
     * @static
     */
    function put($key, $data, $protected=false) {
	if (!GalleryDataCache::isMemoryCachingEnabled()) {
	    return;
	}

	$cache =& GalleryDataCache::_getCache();
	$cache['keys'][$key] = $data;
	if ($protected) {
	    $cache['protected'][$key] = 1;
	} else {
	    $cache['timestamps'][$key] = time() + microtime();
	}

	GalleryDataCache::_performMaintenance();
    }

    /**
     * Remove data from the cache
     *
     * @param string the key
     * @param mixed the data
     * @static
     */
    function remove($key) {
	if (!GalleryDataCache::isMemoryCachingEnabled()) {
	    return;
	}

	$cache =& GalleryDataCache::_getCache();
	unset($cache['timestamps'][$key]);
	unset($cache['keys'][$key]);
	unset($cache['protected'][$key]);
    }

    /**
     * Remove data from the cache
     *
     * @param string the key
     * @param mixed the data
     * @static
     */
    function removeByPattern($pattern) {
	if (!GalleryDataCache::isMemoryCachingEnabled()) {
	    return;
	}

	$cache =& GalleryDataCache::_getCache();
	foreach (preg_grep("/$pattern/", array_keys($cache['keys'])) as $key) {
	    unset($cache['timestamps'][$key]);
	    unset($cache['keys'][$key]);
	    unset($cache['protected'][$key]);
	}
    }

    /**
     * Store a reference to the data in the cache
     *
     * You must provide a unique key.  Existing keys are overwritten.
     *
     * @param string the key
     * @param mixed the data
     * @param boolean should this key survive a reset call?
     * @static
     */
    function putByReference($key, &$data, $protected=false) {
	if (!GalleryDataCache::isMemoryCachingEnabled()) {
	    return;
	}

	$cache =& GalleryDataCache::_getCache();
	$cache['keys'][$key] =& $data;
	if ($protected) {
	    $cache['protected'][$key] = 1;
	} else {
	    $cache['timestamps'][$key] = time() + microtime();
	}

	GalleryDataCache::_performMaintenance();
    }

    /**
     * Perform some pruning of our cache to prevent it from growing too large when we're doing
     * exceptionally long operations like adding many items in one request.
     *
     * @access private
     * @static
     */
    function _performMaintenance() {
	static $count;

	if (!isset($count)) {
	    $count = 0;
	}

	if ($count++ % 150 == 0) {
	    $cache =& GalleryDataCache::_getCache();
	    if (sizeof($cache['timestamps']) > $cache['maxKeys']) {
		$timestamps = $cache['timestamps'];
		asort($timestamps);
		$numToExpire = sizeof($timestamps) - $cache['maxKeys'];
		foreach ($timestamps as $key => $timestamp) {
		    unset($cache['keys'][$key]);
		    unset($cache['timestamps'][$key]);
		    if ($numToExpire-- == 0) {
			break;
		    }
		}
	    }
	}
    }

    /**
     * Retrieve data from the cache
     *
     * @param string the key
     * @return mixed the cached data
     * @static
     */
    function get($key) {
	if (!GalleryDataCache::isMemoryCachingEnabled()) {
	    return null;
	}

	$cache =& GalleryDataCache::_getCache();
	if (!isset($cache['protected'][$key])) {
	    $cache['timestamps'][$key] = time() + microtime();
	}
	return $cache['keys'][$key];
    }

    /**
     * Does the cache contain the key specified?
     *
     * @param string the function/method name
     * @return boolean true if the cache contains the key given
     * @static
     */
    function containsKey($key) {
	if (!GalleryDataCache::isMemoryCachingEnabled()) {
	    return false;
	}

	$cache =& GalleryDataCache::_getCache();
	return isset($cache['keys'][$key]);
    }

    /**
     * Return all the keys in the cache
     *
     * @return array string keys
     * @static
     */
    function getAllKeys() {
	if (!GalleryDataCache::isMemoryCachingEnabled()) {
	    return array();
	}

	$cache =& GalleryDataCache::_getCache();
	return array_keys($cache['keys']);
    }

    /**
     * Empty the cache of all but protected entries
     *
     * @param boolean purge protected also?
     * @static
     */
    function reset($purgeProtected=false) {
	if (!GalleryDataCache::isMemoryCachingEnabled()) {
	    return;
	}

	$cache =& GalleryDataCache::_getCache();

	if ($purgeProtected) {
	    $cache['timestamps'] = array();
	    $cache['keys'] = array();
	} else {
	    foreach (array_keys($cache['keys']) as $key) {
		if (!isset($cache['protected'][$key])) {
		    unset($cache['timestamps'][$key]);
		    unset($cache['keys'][$key]);
		}
	    }
	}
    }

    /**
     * Is caching to disk enabled?
     * @return bool true if it's enabled
     */
    function &isFileCachingEnabled() {
	static $enabled;
	if (!isset($enabled)) {
	    $enabled = true;
	}
	return $enabled;
    }

    /**
     * Turn caching to disk on or off
     * @param bool true or false
     */
    function setFileCachingEnabled($bool) {
	$enabled =& GalleryDataCache::isFileCachingEnabled();
	$enabled = $bool;
    }

    /**
     * Get the file from disk.  PathInfo is of the form that can be passed to getCachePath
     * @see GalleryDataCache::getCachePath
     * @param array the path info
     * @return mixed object data
     */
    function &getFromDisk($pathInfo) {
	$null = null;
	if (!GalleryDataCache::isFileCachingEnabled()) {
	    return $null;
	}

	global $gallery;
	$platform = $gallery->getPlatform();
	$cacheFile = GalleryDataCache::getCachePath($pathInfo);
	if ($platform->file_exists($cacheFile) &&
		$buf = $platform->file_get_contents($cacheFile)) {
	    /* Parse the cache file */
	    $marker = strcspn($buf, '|');
	    foreach (explode(',', substr($buf, 0, $marker)) as $classFile) {
		if ($classFile) {
		    GalleryCoreApi::relativeRequireOnce($classFile);
		}
	    }
	    $data = unserialize(substr($buf, $marker+1));
	    return $data;
	}

	return $null;
    }

    /**
     * Remove the cache file from disk.  PathInfo is of the form that can be passed to getCachePath
     * @see GalleryDataCache::getCachePath
     * @param array the path info
     */
    function removeFromDisk($pathInfo) {
	if (!GalleryDataCache::isFileCachingEnabled()) {
	    return GalleryStatus::success();
	}

	global $gallery;
	$platform = $gallery->getPlatform();
	if ($pathInfo['type'] == 'entity' ||
		$pathInfo['type'] == 'derivative-meta' ||
		$pathInfo['type'] == 'module-data' ||
		isset($pathInfo['id'])) {
	    $cacheFile = GalleryDataCache::getCachePath($pathInfo);
	    if ($platform->file_exists($cacheFile)) {
		if ($platform->is_dir($cacheFile)) {
		    $platform->recursiveRmDir($cacheFile);
		} else {
		    $platform->unlink($cacheFile);
		}
	    }
	} else {
	    if ($pathInfo['type'] == 'module' || $pathInfo['type'] == 'theme') {
		list ($ret, $pluginStatus) = GalleryCoreApi::fetchPluginStatus($pathInfo['type']);
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}

		foreach (array_keys($pluginStatus) as $pluginId) {
		    GalleryDataCache::removeFromDisk(array('type' => $pathInfo['type'],
							   'id' => $pluginId,
							   'itemId' => $pathInfo['itemId']));
		}
	    }
	}
    }

    /**
     * Put the specified data into a cache file from disk.  PathInfo is of the
     * form that can be passed to getCachePath.
     *
     * @param array the path info
     * @param mixed the object data
     * @param array a list of classes that must be loaded in order to retrieve this data
     * @see GalleryDataCache::getCachePath
     */
    function putToDisk($pathInfo, &$data, $requiredClasses=array()) {
	if (!GalleryDataCache::isFileCachingEnabled()) {
	    return;
	}

	global $gallery;
	$cacheFile = GalleryDataCache::getCachePath($pathInfo);
	$platform = $gallery->getPlatform();
	GalleryUtilities::guaranteeDirExists(dirname($cacheFile));

	/* This will either succeed, or leave no trace of its attempt. */
	$platform->atomicWrite(
	    $cacheFile, implode(',', $requiredClasses) . "|" . serialize($data));
    }

    /**
     * For a given id, return a tuple with the breakdown of the id.  The caching
     * mechanism uses this to determine where in the cache tree to place the
     * file.  The breakdown happens according to the digits of the id.  The
     * first element returned is the hundreds digit, the second element is the
     * tens digit.
     *
     *  0..9     =>  0, 0
     *  10..19   =>  0, 1
     *  20..29   =>  0, 2
     *   ...
     *  100..109 =>  1, 0
     *  110..119 =>  1, 1
     *
     * @param int the id
     * @return array the tuple
     */
    function getCacheTuple($id) {
	$id = "$id";
	if ($id > 100) {
	    return array($id[0], $id[1]);
	} else if ($id > 10) {
	    return array('0', $id[0]);
	} else {
	    return array('0', '0');
	}
    }

    /**
     * Given a path info descriptor, return the path to the appropriate cache file
     *
     * Path info contains the following variables:
     *  type:    entity, derivative, derivative-meta, module, theme
     *  itemId:  the item id
     *  id:      (module, theme only) a refinement of the type
     *
     * @return string the path
     */
    function getCachePath($pathInfo) {
	global $gallery;

	$base = $gallery->getConfig('data.gallery.cache');
	$cacheFile = null;
	switch ($pathInfo['type']) {
	case 'entity':
	    list ($first, $second) = GalleryDataCache::getCacheTuple($pathInfo['itemId']);
	    $cacheFile = sprintf('%sentity/%s/%s/%d.inc',
				 $base, $first, $second, $pathInfo['itemId']);
	    break;

	case 'derivative':
	    list ($first, $second) = GalleryDataCache::getCacheTuple($pathInfo['itemId']);
	    $cacheFile = sprintf('%sderivative/%s/%s/%d.dat',
				 $base, $first, $second, $pathInfo['itemId']);
	    break;

	case 'derivative-relative':
	    list ($first, $second) = GalleryDataCache::getCacheTuple($pathInfo['itemId']);
	    $cacheFile = sprintf('derivative/%s/%s/%d.dat',
				 $first, $second, $pathInfo['itemId']);
	    break;

	case 'derivative-meta':
	    list ($first, $second) = GalleryDataCache::getCacheTuple($pathInfo['itemId']);
	    $cacheFile = sprintf('%sderivative/%s/%s/%d-meta.inc',
				 $base, $first, $second, $pathInfo['itemId']);
	    break;

	case 'fast-download':
	    list ($first, $second) = GalleryDataCache::getCacheTuple($pathInfo['itemId']);
	    $cacheFile = sprintf('%sderivative/%s/%s/%d-fast.inc',
				 $base, $first, $second, $pathInfo['itemId']);
	    break;

	case 'module':
	case 'theme':
	    if (isset($pathInfo['id'])) {
		if (strstr($pathInfo['id'], '..') !== false) {
		    $pathInfo['id'] = '0';
		}
		if (isset($pathInfo['itemId'])) {
		    if (strstr($pathInfo['itemId'], '..') !== false) {
			$pathInfo['itemId'] = '0';
		    }
		    list ($first, $second) = GalleryDataCache::getCacheTuple($pathInfo['itemId']);
		    $cacheFile = sprintf('%s%s/%s/%s/%s/%s.inc',
					 $base, $pathInfo['type'], $pathInfo['id'],
					 $first, $second, $pathInfo['itemId']);
		} else {
		    $cacheFile = sprintf('%s%s/%s', $base, $pathInfo['type'], $pathInfo['id']);
		}
	    }
	    break;

	case 'module-data':
	    if (strstr($pathInfo['module'], '..') !== false) {
		$pathInfo['module'] = '0';
	    }
	    if (isset($pathInfo['itemId'])) {
		list ($first, $second) = GalleryDataCache::getCacheTuple($pathInfo['itemId']);
		$cacheFile = sprintf('%s%s/%s/%s/%s/%d.dat',
				     $base, 'module', $pathInfo['module'],
				     $first, $second, $pathInfo['itemId']);
	    } else {
		$cacheFile = sprintf('%s%s/%s/', $base, 'module', $pathInfo['module']);
	    }
	    break;
	}

	return $cacheFile;
    }

    /**
     * Store the given permission => ids mapping in the session cache
     * @param mixed item ids (can be an array of ids or a single id)
     * @param string the permission
     */
    function cachePermissions($ids, $permission) {
	global $gallery;
	$session =& $gallery->getSession();
	if (!isset($session)) {
	    /* No session means we've got no cache */
	    return;
	}

	if (!is_array($ids)) {
	    $ids = array($ids);
	}

	$permissions = $session->get('permissionCache');

	/*
	 * We want to put all the permissions that we cache in this request into one entry in
	 * the session.  However we're going to get several separate requests.  So start by
	 * pruning down the total number of cached permission sets, then create a new set
	 * and add all cached values to that.
	 */
	static $initialized;
	if (!isset($initialized)) {
	    if (!isset($permissions)) {
		$permissions = array();
	    }

	    array_unshift($permissions, array());
	    /* Trim down the cache */
	    $max = 10;
	    if (sizeof($permissions) > $max) {
		array_splice($permissions, -1, sizeof($permissions) - $max);
	    }

	    $initialized = 1;
	}

	/* Add our new data to the head of the list */
	foreach ($ids as $id) {
	    $permissions[0][$permission][$id] = 1;
	}

	$session->put('permissionCache', $permissions);
    }

    /**
     * Look up the given permission in the cache.  Return true if the permission
     * exists in the cache, false if it doesn't.  A return of false doesn't mean
     * that the user doesn't have the permission -- just that it's not in the
     * cache.
     *
     * @param int the item id
     * @param string the permission
     * @param bool true or false
     */
    function hasPermission($id, $permission) {
	global $gallery;
	$session =& $gallery->getSession();
	if (!isset($session)) {
	    return;
	}

	$permissions = $session->get('permissionCache');
	/*
	 * Since we add all new data to the head of the list, the odds are good
	 * that we should find our answer in the first iteration of this loop.
	 */
	for ($i = 0; $i < sizeof($permissions); $i++) {
	    if (isset($permissions[$i][$permission][$id])) {
		return true;
	    }
	}

	return false;
    }

    /**
     * Clear permission cache for active user.
     */
    function clearPermissionCache() {
	global $gallery;
	$session =& $gallery->getSession();
	if (isset($session)) {
	    $session->remove('permissionCache');
	}
    }
}
?>
