bin = $bin;
$filecache_directory = filecache_directory();
$t = get_t();
// Check for problems with filecache_directory
$hint = FALSE;
if (empty($filecache_directory)) {
$hint = t('Your web server is not Apache and so default filecache_directory cannot be used.');
}
else {
if (!is_dir($filecache_directory)) {
if (!file_exists($filecache_directory)) {
// Directory does not exist. Try to create it.
if (!mkdir($filecache_directory, 0777, TRUE)) {
$hint = $t('%dir does not exist and filecache.inc was not able to create it probably due to permission problem.', array('%dir' => $filecache_directory));
}
if (!chmod($filecache_directory, 0777)) {
// insist that $filecache_directory must have 777 access mode
// or better not exist at all
rmdir($filecache_directory);
$hint = $t('%dir does not exist, filecache.inc successfully created it but chmod 777 failed.', array('%dir' => $filecache_directory));
}
}
else {
$hint = $t('%dir is not directory.', array('%dir' => $filecache_directory));
}
}
elseif (!is_writable($filecache_directory)) {
$hint = $t('%dir is directory but PHP cannot write to it.', array('%dir' => $filecache_directory));
}
}
if ($hint) {
?>
dirname(__FILE__) . '/README.txt')) ?>
$hint)) ?>
directory = $filecache_directory;
$this->prefix = $this->directory . '/' . $bin . '-';
}
/**
* Make cache ID usable for file name.
*
* @param $cid
* Cache ID.
* @return
* String that is derived from $cid and can be used as file name.
*/
function encode_cid($cid) {
// Use urlencode(), but turn the
// encoded ':' and '/' back into ordinary characters since they're used so
// often. (Especially ':', but '/' is used in cache_menu.)
// We can't turn them back into their own characters though; both are
// considered unsafe in filenames. So turn : -> and / -> ^
$safe_cid = str_replace(array('%2F', '%3A'), array('^', ' '), urlencode($cid));
if (strlen($safe_cid) > FILECACHE_CID_FILENAME_MAX) {
$safe_cid =
substr($safe_cid, 0, FILECACHE_CID_FILENAME_POS_BEFORE_MD5) .
'%_' .
md5(substr($safe_cid, FILECACHE_CID_FILENAME_POS_BEFORE_MD5));
}
return $safe_cid;
}
/**
* Return data from the persistent cache. Data may be stored as either plain
* text or as serialized data. cache_get will automatically return
* unserialized objects and arrays.
*
* @param $cid
* The cache ID of the data to retrieve.
* @return
* The cache or FALSE on failure.
*/
function get($cid) {
$filename = $this->prefix . $this->encode_cid($cid);
// XXX should be in getMultiple() and get() to call getMultiple()
$this->delete_flushed();
// Use @ because cache entry may not exist
$content = @file_get_contents($filename);
if ($content === FALSE) {
return FALSE;
}
$cache = @unserialize($content);
if ($cache === FALSE) {
// we are in the middle of cache_set
$fh = fopen($filename, 'rb');
if ($fh === FALSE) {
return FALSE;
}
if (flock($fh, LOCK_SH) === FALSE) {
fclose($fh);
return FALSE;
}
$cache = @unserialize(@stream_get_contents($fh));
if ($cache === FALSE ||
flock($fh, LOCK_UN) === FALSE ||
fclose($fh) === FALSE) {
unlink($filename); // remove broken file
flock($fh, LOCK_UN);
fclose($fh);
return FALSE;
}
}
// XXX Should reproduce the cache_lifetime / cache_flush_$bin logic
$cache_flush = variable_get('filecache_flush_' . $this->bin, 0);
if ($cache->expire != CACHE_TEMPORARY && // XXX how to handle this?
$cache->expire != CACHE_PERMANENT &&
($cache->expire < REQUEST_TIME ||
($cache_flush && $cache->created < $cache_flush))) {
unlink($filename);
return FALSE;
}
// Some systems don't update access time so we do it this way
// XXX There's a chance that file no longer exists at this point
// XXX but it's ok because we deal fine with broken cache entries
// XXX should check only once in a page request if we have such
// XXX filesystem and set $this->touch so that here we now what to do
// XXX should be configurable
// touch($filename);
// XXX should assert($cache->cid == $cid)
return $cache;
}
/**
* Return data from the persistent cache when given an array of cache IDs.
*
* @param $cids
* An array of cache IDs for the data to retrieve. This is passed by
* reference, and will have the IDs successfully returned from cache
* removed.
* @return
* An array of the items successfully returned from cache indexed by cid.
*/
function getMultiple(&$cids) {
$results = array();
foreach ($cids as $cid) {
$cache = $this->get($cid);
if ($cache !== FALSE) {
$results[$cid] = $cache;
unset($cids[$cid]);
}
}
return $results;
}
/**
* Store data in the persistent cache.
*
* @param $cid
* The cache ID of the data to store.
* @param $data
* The data to store in the cache. Complex data types will be automatically
* serialized before insertion.
* Strings will be stored as plain text and not serialized.
* @param $expire
* One of the following values:
* - CACHE_PERMANENT: Indicates that the item should never be removed unless
* explicitly told to using cache_clear_all() with a cache ID.
* - CACHE_TEMPORARY: Indicates that the item should be removed at the next
* general cache wipe.
* - A Unix timestamp: Indicates that the item should be kept at least until
* the given time, after which it behaves like CACHE_TEMPORARY.
*/
function set($cid, $data, $expire = CACHE_PERMANENT) {
$filename = $this->prefix . $this->encode_cid($cid);
// Open file for that entry, handling errors that may arise
$fh = @fopen($filename, 'r+b');
if ($fh === FALSE) {
// If file doesn't exist, create it with a+w permissions
$fh = fopen($filename, 'c+b');
if ($fh !== FALSE) {
if (!chmod($filename, 0666)) {
watchdog('filecache', 'Cannot chmod %filename',
array('%filename' => $filename), WATCHDOG_CRITICAL);
return;
}
}
else {
// most likely permission error - report it as critical error
watchdog('filecache', 'Cannot open %filename',
array('%filename' => $filename), WATCHDOG_CRITICAL);
return;
}
}
// Our safeguard for simultaneous writing in the same file
if (flock($fh, LOCK_EX) === FALSE) {
fclose($fh);
return;
}
$cache = new StdClass;
$cache->cid = $cid;
$cache->created = REQUEST_TIME;
$cache->expire = $expire;
$cache->data = $data;
if (ftruncate($fh, 0) === FALSE ||
fwrite($fh, serialize($cache)) === FALSE ||
flock($fh, LOCK_UN) === FALSE ||
fclose($fh) === FALSE) {
// XXX should not happen -> cleanup
unlink($filename);
flock($fh, LOCK_UN);
fclose($fh);
return;
}
}
/**
* Expire data from the cache. If called without arguments, expirable
* entries will be cleared from the cache_page and cache_block bins.
*
* @param $cid
* If set, the cache ID to delete. Otherwise, all cache entries that can
* expire are deleted.
* @param $wildcard
* If set to TRUE, the $cid is treated as a substring
* to match rather than a complete ID. The match is a right hand
* match. If '*' is given as $cid, the bin $bin will be emptied.
*/
function clear($cid = NULL, $wildcard = FALSE) {
global $user;
// parts are shamelessy copied from includes/cache.inc
if (empty($cid)) {
if (variable_get('cache_lifetime', 0)) {
// We store the time in the current user's $user->cache variable which
// will be saved into the sessions bin by _drupal_session_write(). We then
// simulate that the cache was flushed for this user by not returning
// cached data that was cached before the timestamp.
$user->cache = REQUEST_TIME;
$cache_flush = variable_get('cache_flush_' . $this->bin, 0);
if ($cache_flush == 0) {
// This is the first request to clear the cache, start a timer.
variable_set('cache_flush_' . $this->bin, REQUEST_TIME);
}
elseif (REQUEST_TIME > ($cache_flush + variable_get('cache_lifetime', 0))) {
// Clear the cache for everyone, cache_lifetime seconds have
// passed since the first request to clear the cache.
$this->delete_expired();
variable_set('cache_flush_' . $this->bin, 0);
}
}
else {
// No minimum cache lifetime, flush all temporary cache entries now.
$this->delete_expired();
}
}
else {
if ($wildcard) {
if ($cid == '*') {
$this->delete_wildcard('');
}
else {
$this->delete_wildcard($cid);
}
}
elseif (is_array($cid)) {
foreach ($cid as $one_cid) {
$this->delete_one($one_cid);
}
}
else {
$this->delete_one($cid);
}
}
}
/**
* Delete a single cache object.
*
* @param $cid
* Cache ID.
*/
protected function delete_one($cid) {
$filename = $this->prefix . $this->encode_cid($cid);
@unlink($filename);
}
/**
* List of all cache objects with specified prefix in their name.
*
* @param $cid_prefix
* Prefix for cache IDs to delete.
*/
protected function all($cid_prefix = '') {
$list = array();
$filename_prefix = $this->bin . '-' . $this->encode_cid($cid_prefix);
$filename_prefix_len = strlen($filename_prefix);
if (is_dir($this->directory)) {
$cwd = getcwd();
chdir($this->directory);
$dh = opendir('.');
while (($filename = readdir($dh)) !== FALSE) {
if (strncmp($filename, $filename_prefix, $filename_prefix_len) === 0) {
$list[] = $filename;
}
}
closedir($dh);
chdir($cwd);
}
return $list;
}
/**
* Delete all cache objects witch specified prefix in their name.
*
* @param $cid_prefix
* Prefix for cache IDs to delete.
*/
protected function delete_wildcard($cid_prefix) {
foreach ($this->all($cid_prefix) as $filename) {
@unlink ($this->directory . '/' . $filename);
}
}
/**
* Delete expired cache entries.
*/
protected function delete_expired() {
$cwd = getcwd();
chdir($this->directory);
foreach ($this->all() as $filename) {
// XXX reads all entries XXX
$content = @file_get_contents($filename);
if ($content === FALSE) {
continue;
}
$cache = @unserialize($content);
if ($cache === FALSE) {
continue;
}
if ($cache->expire == CACHE_PERMANENT) {
continue;
}
$expiry_date = $cache->expire;
if ($cache->expire == CACHE_TEMPORARY) {
$expiry_date = $cache->created + variable_get('cache_lifetime', 0);
}
if ($expiry_date < REQUEST_TIME) {
@unlink($filename);
}
} // foreach $filename
chdir($cwd);
}
/**
* Delete flushed cache entries.
*/
protected function delete_flushed() {
static $recursion = FALSE; // XXX how cache.inc survives this?
if ($recursion) {
return;
}
$recursion = TRUE;
// Garbage collection necessary when enforcing a minimum cache lifetime.
$cache_flush = variable_get('cache_flush_' . $this->bin, 0);
if ($cache_flush && ($cache_flush + variable_get('cache_lifetime', 0) <= REQUEST_TIME)) {
// Reset the variable immediately to prevent a meltdown in heavy load situations.
variable_set('cache_flush_' . $this->bin, 0);
// Time to flush old cache data
$cwd = getcwd();
chdir($this->directory);
foreach ($this->all() as $filename) {
// XXX reads all entries XXX
$content = @file_get_contents($filename);
if ($content === FALSE) {
continue;
}
$cache = @unserialize($content);
if ($cache === FALSE) {
continue;
}
if ($cache->expire != CACHE_PERMANENT &&
$cache->expire <= $cache_flush) {
@unlink($filename);
}
} // foreach $filename
chdir($cwd);
} // if $cache_flush
$recursion = FALSE;
}
/**
* Check if a cache bin is empty.
*
* A cache bin is considered empty if it does not contain any valid data for
* any cache ID.
*
* @return
* TRUE if the cache bin specified is empty.
*/
function isEmpty() {
return count($this->all()) == 0;
}
}