From 21d3bf3b60bcf0e88165ef79c718b2efb1f5364c Mon Sep 17 00:00:00 2001 From: fulmeek <36341513+fulmeek@users.noreply.github.com> Date: Mon, 29 Apr 2019 20:12:43 +0200 Subject: [PATCH] caches: Refactor the API (#1060) - For consistency, functions should always return null on non-existing data. - WordPressPluginUpdateBridge appears to have used its own cache instance in the past. Obviously not used anymore. - Since $key can be anything, the cache implementation must ensure to assign the related data reliably; most commonly by serializing and hashing the key in an appropriate way. - Even though the default path for storage is perfectly fine, some people may want to use a different location. This is an example how a cache implementation is responsible for its requirements. --- actions/DisplayAction.php | 4 +- bridges/ElloBridge.php | 4 +- bridges/WordPressPluginUpdateBridge.php | 12 ----- caches/FileCache.php | 72 +++++++++++++------------ caches/SQLiteCache.php | 60 ++++++++++++++------- config.default.ini.php | 5 ++ lib/CacheInterface.php | 40 +++++++++----- lib/contents.php | 8 +-- 8 files changed, 121 insertions(+), 84 deletions(-) diff --git a/actions/DisplayAction.php b/actions/DisplayAction.php index b223b757..a1b106f5 100644 --- a/actions/DisplayAction.php +++ b/actions/DisplayAction.php @@ -86,9 +86,9 @@ class DisplayAction extends ActionAbstract { // Initialize cache $cache = Cache::create(Configuration::getConfig('cache', 'type')); - $cache->setPath(PATH_CACHE); + $cache->setScope(''); $cache->purgeCache(86400); // 24 hours - $cache->setParameters($cache_params); + $cache->setKey($cache_params); $items = array(); $infos = array(); diff --git a/bridges/ElloBridge.php b/bridges/ElloBridge.php index 45d33a53..1f66edc3 100644 --- a/bridges/ElloBridge.php +++ b/bridges/ElloBridge.php @@ -121,8 +121,8 @@ class ElloBridge extends BridgeAbstract { private function getAPIKey() { $cache = Cache::create(Configuration::getConfig('cache', 'type')); - $cache->setPath(PATH_CACHE); - $cache->setParameters(['key']); + $cache->setScope(get_called_class()); + $cache->setKey(['key']); $key = $cache->loadData(); if($key == null) { diff --git a/bridges/WordPressPluginUpdateBridge.php b/bridges/WordPressPluginUpdateBridge.php index 51ddd5b7..9101c4ee 100644 --- a/bridges/WordPressPluginUpdateBridge.php +++ b/bridges/WordPressPluginUpdateBridge.php @@ -71,16 +71,4 @@ class WordPressPluginUpdateBridge extends BridgeAbstract { return parent::getName(); } - - private function getCachedDate($url){ - Debug::log('getting pubdate from url ' . $url . ''); - // Initialize cache - $cache = Cache::create(Configuration::getConfig('cache', 'type')); - $cache->setPath(PATH_CACHE . 'pages/'); - $params = [$url]; - $cache->setParameters($params); - // Get cachefile timestamp - $time = $cache->getTime(); - return ($time !== false ? $time : time()); - } } diff --git a/caches/FileCache.php b/caches/FileCache.php index 04d08a25..166ecdb5 100644 --- a/caches/FileCache.php +++ b/caches/FileCache.php @@ -3,20 +3,21 @@ * Cache with file system */ class FileCache implements CacheInterface { - protected $path; - protected $param; + protected $key; public function loadData(){ if(file_exists($this->getCacheFile())) { return unserialize(file_get_contents($this->getCacheFile())); } + + return null; } - public function saveData($datas){ + public function saveData($data){ // Notice: We use plain serialize() here to reduce memory footprint on // large input data. - $writeStream = file_put_contents($this->getCacheFile(), serialize($datas)); + $writeStream = file_put_contents($this->getCacheFile(), serialize($data)); if($writeStream === false) { throw new \Exception('Cannot write the cache... Do you have the right permissions ?'); @@ -29,13 +30,14 @@ class FileCache implements CacheInterface { $cacheFile = $this->getCacheFile(); clearstatcache(false, $cacheFile); if(file_exists($cacheFile)) { - return filemtime($cacheFile); + $time = filemtime($cacheFile); + return ($time !== false) ? $time : null; } - return false; + return null; } - public function purgeCache($duration){ + public function purgeCache($seconds){ $cachePath = $this->getPath(); if(file_exists($cachePath)) { $cacheIterator = new RecursiveIteratorIterator( @@ -47,7 +49,7 @@ class FileCache implements CacheInterface { if(in_array($cacheFile->getBasename(), array('.', '..', '.gitkeep'))) continue; elseif($cacheFile->isFile()) { - if(filemtime($cacheFile->getPathname()) < time() - $duration) + if(filemtime($cacheFile->getPathname()) < time() - $seconds) unlink($cacheFile->getPathname()); } } @@ -55,34 +57,34 @@ class FileCache implements CacheInterface { } /** - * Set cache path + * Set scope * @return self */ - public function setPath($path){ - if(is_null($path) || !is_string($path)) { - throw new \Exception('The given path is invalid!'); + public function setScope($scope){ + if(is_null($scope) || !is_string($scope)) { + throw new \Exception('The given scope is invalid!'); } - $this->path = $path; - - // Make sure path ends with '/' or '\' - $lastchar = substr($this->path, -1, 1); - if($lastchar !== '/' && $lastchar !== '\\') - $this->path .= '/'; - - if(!is_dir($this->path)) - mkdir($this->path, 0755, true); + $this->path = PATH_CACHE . trim($scope, " \t\n\r\0\x0B\\\/") . '/'; return $this; } /** - * Set HTTP GET parameters + * Set key * @return self */ - public function setParameters(array $param){ - $this->param = array_map('strtolower', $param); + public function setKey($key){ + if (!empty($key) && is_array($key)) { + $key = array_map('strtolower', $key); + } + $key = json_encode($key); + if (!is_string($key)) { + throw new \Exception('The given key is invalid!'); + } + + $this->key = $key; return $this; } @@ -90,9 +92,15 @@ class FileCache implements CacheInterface { * Return cache path (and create if not exist) * @return string Cache path */ - protected function getPath(){ + private function getPath(){ if(is_null($this->path)) { - throw new \Exception('Call "setPath" first!'); + throw new \Exception('Call "setScope" first!'); + } + + if(!is_dir($this->path)) { + if (mkdir($this->path, 0755, true) !== true) { + throw new \Exception('Unable to create ' . $this->path); + } } return $this->path; @@ -102,7 +110,7 @@ class FileCache implements CacheInterface { * Get the file name use for cache store * @return string Path to the file cache */ - protected function getCacheFile(){ + private function getCacheFile(){ return $this->getPath() . $this->getCacheName(); } @@ -110,13 +118,11 @@ class FileCache implements CacheInterface { * Determines file name for store the cache * return string */ - protected function getCacheName(){ - if(is_null($this->param)) { - throw new \Exception('Call "setParameters" first!'); + private function getCacheName(){ + if(is_null($this->key)) { + throw new \Exception('Call "setKey" first!'); } - // Change character when making incompatible changes to prevent loading - // errors due to incompatible file contents \|/ - return hash('md5', http_build_query($this->param) . 'A') . '.cache'; + return hash('md5', $this->key) . '.cache'; } } diff --git a/caches/SQLiteCache.php b/caches/SQLiteCache.php index 5cbb3772..7d0f584f 100644 --- a/caches/SQLiteCache.php +++ b/caches/SQLiteCache.php @@ -3,16 +3,25 @@ * Cache based on SQLite 3 */ class SQLiteCache implements CacheInterface { - protected $path; - protected $param; + protected $scope; + protected $key; private $db = null; public function __construct() { - if (!extension_loaded('sqlite3')) + if (!extension_loaded('sqlite3')) { die('"sqlite3" extension not loaded. Please check "php.ini"'); + } - $file = PATH_CACHE . 'cache.sqlite'; + $file = Configuration::getConfig(get_called_class(), 'file'); + if (empty($file)) { + die('Configuration for ' . get_called_class() . ' missing. Please check your config.ini.php'); + } + if (dirname($file) == '.') { + $file = PATH_CACHE . $file; + } elseif (!is_dir(dirname($file))) { + die('Invalid configuration for ' . get_called_class() . '. Please check your config.ini.php'); + } if (!is_file($file)) { $this->db = new SQLite3($file); @@ -39,10 +48,10 @@ class SQLiteCache implements CacheInterface { return null; } - public function saveData($datas){ + public function saveData($data){ $Qupdate = $this->db->prepare('INSERT OR REPLACE INTO storage (key, value, updated) VALUES (:key, :value, :updated)'); $Qupdate->bindValue(':key', $this->getCacheKey()); - $Qupdate->bindValue(':value', serialize($datas)); + $Qupdate->bindValue(':value', serialize($data)); $Qupdate->bindValue(':updated', time()); $Qupdate->execute(); @@ -60,40 +69,53 @@ class SQLiteCache implements CacheInterface { } } - return false; + return null; } - public function purgeCache($duration){ + public function purgeCache($seconds){ $Qdelete = $this->db->prepare('DELETE FROM storage WHERE updated < :expired'); - $Qdelete->bindValue(':expired', time() - $duration); + $Qdelete->bindValue(':expired', time() - $seconds); $Qdelete->execute(); } /** - * Set cache path + * Set scope * @return self */ - public function setPath($path){ - $this->path = $path; + public function setScope($scope){ + if(is_null($scope) || !is_string($scope)) { + throw new \Exception('The given scope is invalid!'); + } + + $this->scope = $scope; return $this; } /** - * Set HTTP GET parameters + * Set key * @return self */ - public function setParameters(array $param){ - $this->param = array_map('strtolower', $param); + public function setKey($key){ + if (!empty($key) && is_array($key)) { + $key = array_map('strtolower', $key); + } + $key = json_encode($key); + + if (!is_string($key)) { + throw new \Exception('The given key is invalid!'); + } + + $this->key = $key; return $this; } //////////////////////////////////////////////////////////////////////////// - protected function getCacheKey(){ - if(is_null($this->param)) { - throw new \Exception('Call "setParameters" first!'); + private function getCacheKey(){ + if(is_null($this->key)) { + throw new \Exception('Call "setKey" first!'); } - return hash('sha1', $this->path . http_build_query($this->param), true); + return hash('sha1', $this->scope . $this->key, true); } } diff --git a/config.default.ini.php b/config.default.ini.php index 394658d6..90498230 100644 --- a/config.default.ini.php +++ b/config.default.ini.php @@ -52,3 +52,8 @@ username = "" ; The password for authentication. Insert this password when prompted for login. ; Use a strong password to prevent others from guessing your login! password = "" + +; --- Cache specific configuration --------------------------------------------- + +[SQLiteCache] +file = "cache.sqlite" diff --git a/lib/CacheInterface.php b/lib/CacheInterface.php index a74fc0dd..091c5f02 100644 --- a/lib/CacheInterface.php +++ b/lib/CacheInterface.php @@ -13,38 +13,54 @@ /** * The cache interface - * - * @todo Add missing function to the interface - * @todo Explain parameters and return values in more detail - * @todo Return self more often (to allow call chaining) */ interface CacheInterface { + /** + * Set scope of the current cache + * + * If $scope is an empty string, the cache is set to a global context. + * + * @param string $scope The scope the data is related to + */ + public function setScope($scope); + + /** + * Set key to assign the current data + * + * Since $key can be anything, the cache implementation must ensure to + * assign the related data reliably; most commonly by serializing and + * hashing the key in an appropriate way. + * + * @param array $key The key the data is related to + */ + public function setKey($key); + /** * Loads data from cache * - * @return mixed The cache data + * @return mixed The cached data or null */ public function loadData(); /** * Stores data to the cache * - * @param mixed $datas The data to store + * @param mixed $data The data to store * @return self The cache object */ - public function saveData($datas); + public function saveData($data); /** - * Returns the timestamp for the curent cache file + * Returns the timestamp for the curent cache data * - * @return int Timestamp + * @return int Timestamp or null */ public function getTime(); /** - * Removes any data that is older than the specified duration from cache + * Removes any data that is older than the specified age from cache * - * @param int $duration The cache duration in seconds + * @param int $seconds The cache age in seconds */ - public function purgeCache($duration); + public function purgeCache($seconds); } diff --git a/lib/contents.php b/lib/contents.php index 4740f5c2..c65d6dfb 100644 --- a/lib/contents.php +++ b/lib/contents.php @@ -46,11 +46,11 @@ function getContents($url, $header = array(), $opts = array()){ // Initialize cache $cache = Cache::create(Configuration::getConfig('cache', 'type')); - $cache->setPath(PATH_CACHE . 'server/'); + $cache->setScope('server'); $cache->purgeCache(86400); // 24 hours (forced) $params = [$url]; - $cache->setParameters($params); + $cache->setKey($params); // Use file_get_contents if in CLI mode with no root certificates defined if(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo'))) { @@ -271,11 +271,11 @@ function getSimpleHTMLDOMCached($url, // Initialize cache $cache = Cache::create(Configuration::getConfig('cache', 'type')); - $cache->setPath(PATH_CACHE . 'pages/'); + $cache->setScope('pages'); $cache->purgeCache(86400); // 24 hours (forced) $params = [$url]; - $cache->setParameters($params); + $cache->setKey($params); // Determine if cached file is within duration $time = $cache->getTime();