690 lines
15 KiB
PHP
690 lines
15 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* @package Joomla.Platform
|
||
|
* @subpackage Cache
|
||
|
*
|
||
|
* @copyright Copyright (C) 2005 - 2013 Open Source Matters, Inc. All rights reserved.
|
||
|
* @license GNU General Public License version 2 or later; see LICENSE
|
||
|
*/
|
||
|
|
||
|
defined('JPATH_PLATFORM') or die;
|
||
|
|
||
|
/**
|
||
|
* File cache storage handler
|
||
|
*
|
||
|
* @package Joomla.Platform
|
||
|
* @subpackage Cache
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
class JCacheStorageFile extends JCacheStorage
|
||
|
{
|
||
|
/**
|
||
|
* @var string
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
protected $_root;
|
||
|
|
||
|
/**
|
||
|
* Constructor
|
||
|
*
|
||
|
* @param array $options Optional parameters
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
public function __construct($options = array())
|
||
|
{
|
||
|
parent::__construct($options);
|
||
|
$this->_root = $options['cachebase'];
|
||
|
}
|
||
|
|
||
|
// NOTE: raw php calls are up to 100 times faster than JFile or JFolder
|
||
|
|
||
|
/**
|
||
|
* Get cached data from a file by id and group
|
||
|
*
|
||
|
* @param string $id The cache data id
|
||
|
* @param string $group The cache data group
|
||
|
* @param boolean $checkTime True to verify cache time expiration threshold
|
||
|
*
|
||
|
* @return mixed Boolean false on failure or a cached data string
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
public function get($id, $group, $checkTime = true)
|
||
|
{
|
||
|
$data = false;
|
||
|
|
||
|
$path = $this->_getFilePath($id, $group);
|
||
|
|
||
|
if ($checkTime == false || ($checkTime == true && $this->_checkExpire($id, $group) === true))
|
||
|
{
|
||
|
if (file_exists($path))
|
||
|
{
|
||
|
$data = file_get_contents($path);
|
||
|
if ($data)
|
||
|
{
|
||
|
// Remove the initial die() statement
|
||
|
$data = str_replace('<?php die("Access Denied"); ?>#x#', '', $data);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $data;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get all cached data
|
||
|
*
|
||
|
* @return array The cached data
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
public function getAll()
|
||
|
{
|
||
|
parent::getAll();
|
||
|
|
||
|
$path = $this->_root;
|
||
|
$folders = $this->_folders($path);
|
||
|
$data = array();
|
||
|
|
||
|
foreach ($folders as $folder)
|
||
|
{
|
||
|
$files = $this->_filesInFolder($path . '/' . $folder);
|
||
|
$item = new JCacheStorageHelper($folder);
|
||
|
|
||
|
foreach ($files as $file)
|
||
|
{
|
||
|
$item->updateSize(filesize($path . '/' . $folder . '/' . $file) / 1024);
|
||
|
}
|
||
|
$data[$folder] = $item;
|
||
|
}
|
||
|
|
||
|
return $data;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Store the data to a file by id and group
|
||
|
*
|
||
|
* @param string $id The cache data id
|
||
|
* @param string $group The cache data group
|
||
|
* @param string $data The data to store in cache
|
||
|
*
|
||
|
* @return boolean True on success, false otherwise
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
public function store($id, $group, $data)
|
||
|
{
|
||
|
$written = false;
|
||
|
$path = $this->_getFilePath($id, $group);
|
||
|
$die = '<?php die("Access Denied"); ?>#x#';
|
||
|
|
||
|
// Prepend a die string
|
||
|
$data = $die . $data;
|
||
|
|
||
|
$_fileopen = @fopen($path, "wb");
|
||
|
|
||
|
if ($_fileopen)
|
||
|
{
|
||
|
$len = strlen($data);
|
||
|
@fwrite($_fileopen, $data, $len);
|
||
|
$written = true;
|
||
|
}
|
||
|
|
||
|
// Data integrity check
|
||
|
if ($written && ($data == file_get_contents($path)))
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove a cached data file by id and group
|
||
|
*
|
||
|
* @param string $id The cache data id
|
||
|
* @param string $group The cache data group
|
||
|
*
|
||
|
* @return boolean True on success, false otherwise
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
public function remove($id, $group)
|
||
|
{
|
||
|
$path = $this->_getFilePath($id, $group);
|
||
|
if (!@unlink($path))
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clean cache for a group given a mode.
|
||
|
*
|
||
|
* @param string $group The cache data group
|
||
|
* @param string $mode The mode for cleaning cache [group|notgroup]
|
||
|
* group mode : cleans all cache in the group
|
||
|
* notgroup mode : cleans all cache not in the group
|
||
|
*
|
||
|
* @return boolean True on success, false otherwise
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
public function clean($group, $mode = null)
|
||
|
{
|
||
|
$return = true;
|
||
|
$folder = $group;
|
||
|
|
||
|
if (trim($folder) == '')
|
||
|
{
|
||
|
$mode = 'notgroup';
|
||
|
}
|
||
|
|
||
|
switch ($mode)
|
||
|
{
|
||
|
case 'notgroup':
|
||
|
$folders = $this->_folders($this->_root);
|
||
|
for ($i = 0, $n = count($folders); $i < $n; $i++)
|
||
|
{
|
||
|
if ($folders[$i] != $folder)
|
||
|
{
|
||
|
$return |= $this->_deleteFolder($this->_root . '/' . $folders[$i]);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 'group':
|
||
|
default:
|
||
|
if (is_dir($this->_root . '/' . $folder))
|
||
|
{
|
||
|
$return = $this->_deleteFolder($this->_root . '/' . $folder);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
return $return;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Garbage collect expired cache data
|
||
|
*
|
||
|
* @return boolean True on success, false otherwise.
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
public function gc()
|
||
|
{
|
||
|
$result = true;
|
||
|
|
||
|
// Files older than lifeTime get deleted from cache
|
||
|
$files = $this->_filesInFolder($this->_root, '', true, true, array('.svn', 'CVS', '.DS_Store', '__MACOSX', 'index.html'));
|
||
|
foreach ($files as $file)
|
||
|
{
|
||
|
$time = @filemtime($file);
|
||
|
if (($time + $this->_lifetime) < $this->_now || empty($time))
|
||
|
{
|
||
|
$result |= @unlink($file);
|
||
|
}
|
||
|
}
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Test to see if the cache storage is available.
|
||
|
*
|
||
|
* @return boolean True on success, false otherwise.
|
||
|
*
|
||
|
* @since 12.1
|
||
|
*/
|
||
|
public static function isSupported()
|
||
|
{
|
||
|
$conf = JFactory::getConfig();
|
||
|
return is_writable($conf->get('cache_path', JPATH_CACHE));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Lock cached item
|
||
|
*
|
||
|
* @param string $id The cache data id
|
||
|
* @param string $group The cache data group
|
||
|
* @param integer $locktime Cached item max lock time
|
||
|
*
|
||
|
* @return boolean True on success, false otherwise.
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
public function lock($id, $group, $locktime)
|
||
|
{
|
||
|
$returning = new stdClass;
|
||
|
$returning->locklooped = false;
|
||
|
|
||
|
$looptime = $locktime * 10;
|
||
|
$path = $this->_getFilePath($id, $group);
|
||
|
|
||
|
$_fileopen = @fopen($path, "r+b");
|
||
|
|
||
|
if ($_fileopen)
|
||
|
{
|
||
|
$data_lock = @flock($_fileopen, LOCK_EX);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$data_lock = false;
|
||
|
}
|
||
|
|
||
|
if ($data_lock === false)
|
||
|
{
|
||
|
|
||
|
$lock_counter = 0;
|
||
|
|
||
|
// Loop until you find that the lock has been released.
|
||
|
// That implies that data get from other thread has finished
|
||
|
while ($data_lock === false)
|
||
|
{
|
||
|
|
||
|
if ($lock_counter > $looptime)
|
||
|
{
|
||
|
$returning->locked = false;
|
||
|
$returning->locklooped = true;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
usleep(100);
|
||
|
$data_lock = @flock($_fileopen, LOCK_EX);
|
||
|
$lock_counter++;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
$returning->locked = $data_lock;
|
||
|
|
||
|
return $returning;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Unlock cached item
|
||
|
*
|
||
|
* @param string $id The cache data id
|
||
|
* @param string $group The cache data group
|
||
|
*
|
||
|
* @return boolean True on success, false otherwise.
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
public function unlock($id, $group = null)
|
||
|
{
|
||
|
$path = $this->_getFilePath($id, $group);
|
||
|
|
||
|
$_fileopen = @fopen($path, "r+b");
|
||
|
|
||
|
if ($_fileopen)
|
||
|
{
|
||
|
$ret = @flock($_fileopen, LOCK_UN);
|
||
|
@fclose($_fileopen);
|
||
|
}
|
||
|
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check to make sure cache is still valid, if not, delete it.
|
||
|
*
|
||
|
* @param string $id Cache key to expire.
|
||
|
* @param string $group The cache data group.
|
||
|
*
|
||
|
* @return boolean False if not valid
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
protected function _checkExpire($id, $group)
|
||
|
{
|
||
|
$path = $this->_getFilePath($id, $group);
|
||
|
|
||
|
// Check prune period
|
||
|
if (file_exists($path))
|
||
|
{
|
||
|
$time = @filemtime($path);
|
||
|
if (($time + $this->_lifetime) < $this->_now || empty($time))
|
||
|
{
|
||
|
@unlink($path);
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get a cache file path from an id/group pair
|
||
|
*
|
||
|
* @param string $id The cache data id
|
||
|
* @param string $group The cache data group
|
||
|
*
|
||
|
* @return string The cache file path
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
protected function _getFilePath($id, $group)
|
||
|
{
|
||
|
$name = $this->_getCacheId($id, $group);
|
||
|
$dir = $this->_root . '/' . $group;
|
||
|
|
||
|
// If the folder doesn't exist try to create it
|
||
|
if (!is_dir($dir))
|
||
|
{
|
||
|
|
||
|
// Make sure the index file is there
|
||
|
$indexFile = $dir . '/index.html';
|
||
|
@ mkdir($dir) && file_put_contents($indexFile, '<!DOCTYPE html><title></title>');
|
||
|
}
|
||
|
|
||
|
// Make sure the folder exists
|
||
|
if (!is_dir($dir))
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
return $dir . '/' . $name . '.php';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Quickly delete a folder of files
|
||
|
*
|
||
|
* @param string $path The path to the folder to delete.
|
||
|
*
|
||
|
* @return boolean True on success.
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
protected function _deleteFolder($path)
|
||
|
{
|
||
|
// Sanity check
|
||
|
if (!$path || !is_dir($path) || empty($this->_root))
|
||
|
{
|
||
|
// Bad programmer! Bad Bad programmer!
|
||
|
JLog::add('JCacheStorageFile::_deleteFolder ' . JText::_('JLIB_FILESYSTEM_ERROR_DELETE_BASE_DIRECTORY'), JLog::WARNING, 'jerror');
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
$path = $this->_cleanPath($path);
|
||
|
|
||
|
// Check to make sure path is inside cache folder, we do not want to delete Joomla root!
|
||
|
$pos = strpos($path, $this->_cleanPath($this->_root));
|
||
|
|
||
|
if ($pos === false || $pos > 0)
|
||
|
{
|
||
|
JLog::add('JCacheStorageFile::_deleteFolder' . JText::sprintf('JLIB_FILESYSTEM_ERROR_PATH_IS_NOT_A_FOLDER', $path), JLog::WARNING, 'jerror');
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Remove all the files in folder if they exist; disable all filtering
|
||
|
$files = $this->_filesInFolder($path, '.', false, true, array(), array());
|
||
|
|
||
|
if (!empty($files) && !is_array($files))
|
||
|
{
|
||
|
if (@unlink($files) !== true)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
elseif (!empty($files) && is_array($files))
|
||
|
{
|
||
|
|
||
|
foreach ($files as $file)
|
||
|
{
|
||
|
$file = $this->_cleanPath($file);
|
||
|
|
||
|
// In case of restricted permissions we zap it one way or the other
|
||
|
// as long as the owner is either the webserver or the ftp
|
||
|
if (@unlink($file))
|
||
|
{
|
||
|
// Do nothing
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$filename = basename($file);
|
||
|
JLog::add('JCacheStorageFile::_deleteFolder' . JText::sprintf('JLIB_FILESYSTEM_DELETE_FAILED', $filename), JLog::WARNING, 'jerror');
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Remove sub-folders of folder; disable all filtering
|
||
|
$folders = $this->_folders($path, '.', false, true, array(), array());
|
||
|
|
||
|
foreach ($folders as $folder)
|
||
|
{
|
||
|
if (is_link($folder))
|
||
|
{
|
||
|
// Don't descend into linked directories, just delete the link.
|
||
|
if (@unlink($folder) !== true)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
elseif ($this->_deleteFolder($folder) !== true)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// In case of restricted permissions we zap it one way or the other
|
||
|
// as long as the owner is either the webserver or the ftp
|
||
|
if (@rmdir($path))
|
||
|
{
|
||
|
$ret = true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
JLog::add('JCacheStorageFile::_deleteFolder' . JText::sprintf('JLIB_FILESYSTEM_ERROR_FOLDER_DELETE', $path), JLog::WARNING, 'jerror');
|
||
|
$ret = false;
|
||
|
}
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function to strip additional / or \ in a path name
|
||
|
*
|
||
|
* @param string $path The path to clean
|
||
|
* @param string $ds Directory separator (optional)
|
||
|
*
|
||
|
* @return string The cleaned path
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
protected function _cleanPath($path, $ds = DIRECTORY_SEPARATOR)
|
||
|
{
|
||
|
$path = trim($path);
|
||
|
|
||
|
if (empty($path))
|
||
|
{
|
||
|
$path = $this->_root;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Remove double slashes and backslahses and convert all slashes and backslashes to DIRECTORY_SEPARATOR
|
||
|
$path = preg_replace('#[/\\\\]+#', $ds, $path);
|
||
|
}
|
||
|
|
||
|
return $path;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Utility function to quickly read the files in a folder.
|
||
|
*
|
||
|
* @param string $path The path of the folder to read.
|
||
|
* @param string $filter A filter for file names.
|
||
|
* @param mixed $recurse True to recursively search into sub-folders, or an
|
||
|
* integer to specify the maximum depth.
|
||
|
* @param boolean $fullpath True to return the full path to the file.
|
||
|
* @param array $exclude Array with names of files which should not be shown in
|
||
|
* the result.
|
||
|
* @param array $excludefilter Array of folder names to exclude
|
||
|
*
|
||
|
* @return array Files in the given folder.
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
protected function _filesInFolder($path, $filter = '.', $recurse = false, $fullpath = false
|
||
|
, $exclude = array('.svn', 'CVS', '.DS_Store', '__MACOSX'), $excludefilter = array('^\..*', '.*~'))
|
||
|
{
|
||
|
$arr = array();
|
||
|
|
||
|
// Check to make sure the path valid and clean
|
||
|
$path = $this->_cleanPath($path);
|
||
|
|
||
|
// Is the path a folder?
|
||
|
if (!is_dir($path))
|
||
|
{
|
||
|
JLog::add('JCacheStorageFile::_filesInFolder' . JText::sprintf('JLIB_FILESYSTEM_ERROR_PATH_IS_NOT_A_FOLDER', $path), JLog::WARNING, 'jerror');
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Read the source directory.
|
||
|
if (!($handle = @opendir($path)))
|
||
|
{
|
||
|
return $arr;
|
||
|
}
|
||
|
|
||
|
if (count($excludefilter))
|
||
|
{
|
||
|
$excludefilter = '/(' . implode('|', $excludefilter) . ')/';
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$excludefilter = '';
|
||
|
}
|
||
|
while (($file = readdir($handle)) !== false)
|
||
|
{
|
||
|
if (($file != '.') && ($file != '..') && (!in_array($file, $exclude)) && (!$excludefilter || !preg_match($excludefilter, $file)))
|
||
|
{
|
||
|
$dir = $path . '/' . $file;
|
||
|
$isDir = is_dir($dir);
|
||
|
if ($isDir)
|
||
|
{
|
||
|
if ($recurse)
|
||
|
{
|
||
|
if (is_int($recurse))
|
||
|
{
|
||
|
$arr2 = $this->_filesInFolder($dir, $filter, $recurse - 1, $fullpath);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$arr2 = $this->_filesInFolder($dir, $filter, $recurse, $fullpath);
|
||
|
}
|
||
|
|
||
|
$arr = array_merge($arr, $arr2);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if (preg_match("/$filter/", $file))
|
||
|
{
|
||
|
if ($fullpath)
|
||
|
{
|
||
|
$arr[] = $path . '/' . $file;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$arr[] = $file;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
closedir($handle);
|
||
|
|
||
|
return $arr;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Utility function to read the folders in a folder.
|
||
|
*
|
||
|
* @param string $path The path of the folder to read.
|
||
|
* @param string $filter A filter for folder names.
|
||
|
* @param mixed $recurse True to recursively search into sub-folders, or an integer to specify the maximum depth.
|
||
|
* @param boolean $fullpath True to return the full path to the folders.
|
||
|
* @param array $exclude Array with names of folders which should not be shown in the result.
|
||
|
* @param array $excludefilter Array with regular expressions matching folders which should not be shown in the result.
|
||
|
*
|
||
|
* @return array Folders in the given folder.
|
||
|
*
|
||
|
* @since 11.1
|
||
|
*/
|
||
|
protected function _folders($path, $filter = '.', $recurse = false, $fullpath = false
|
||
|
, $exclude = array('.svn', 'CVS', '.DS_Store', '__MACOSX'), $excludefilter = array('^\..*'))
|
||
|
{
|
||
|
$arr = array();
|
||
|
|
||
|
// Check to make sure the path valid and clean
|
||
|
$path = $this->_cleanPath($path);
|
||
|
|
||
|
// Is the path a folder?
|
||
|
if (!is_dir($path))
|
||
|
{
|
||
|
JLog::add('JCacheStorageFile::_folders' . JText::sprintf('JLIB_FILESYSTEM_ERROR_PATH_IS_NOT_A_FOLDER', $path), JLog::WARNING, 'jerror');
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Read the source directory
|
||
|
if (!($handle = @opendir($path)))
|
||
|
{
|
||
|
return $arr;
|
||
|
}
|
||
|
|
||
|
if (count($excludefilter))
|
||
|
{
|
||
|
$excludefilter_string = '/(' . implode('|', $excludefilter) . ')/';
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$excludefilter_string = '';
|
||
|
}
|
||
|
while (($file = readdir($handle)) !== false)
|
||
|
{
|
||
|
if (($file != '.') && ($file != '..')
|
||
|
&& (!in_array($file, $exclude))
|
||
|
&& (empty($excludefilter_string) || !preg_match($excludefilter_string, $file)))
|
||
|
{
|
||
|
$dir = $path . '/' . $file;
|
||
|
$isDir = is_dir($dir);
|
||
|
if ($isDir)
|
||
|
{
|
||
|
// Removes filtered directories
|
||
|
if (preg_match("/$filter/", $file))
|
||
|
{
|
||
|
if ($fullpath)
|
||
|
{
|
||
|
$arr[] = $dir;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$arr[] = $file;
|
||
|
}
|
||
|
}
|
||
|
if ($recurse)
|
||
|
{
|
||
|
if (is_int($recurse))
|
||
|
{
|
||
|
$arr2 = $this->_folders($dir, $filter, $recurse - 1, $fullpath, $exclude, $excludefilter);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$arr2 = $this->_folders($dir, $filter, $recurse, $fullpath, $exclude, $excludefilter);
|
||
|
}
|
||
|
|
||
|
$arr = array_merge($arr, $arr2);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
closedir($handle);
|
||
|
|
||
|
return $arr;
|
||
|
}
|
||
|
}
|