diff options
Diffstat (limited to 'lib/classes/cache')
| -rw-r--r-- | lib/classes/cache/Cache.php | 213 | ||||
| -rw-r--r-- | lib/classes/cache/DbCache.php | 143 | ||||
| -rw-r--r-- | lib/classes/cache/Exception.php | 27 | ||||
| -rw-r--r-- | lib/classes/cache/Factory.php | 206 | ||||
| -rw-r--r-- | lib/classes/cache/FileCache.php | 278 | ||||
| -rw-r--r-- | lib/classes/cache/InvalidCacheArgumentException.php | 28 | ||||
| -rw-r--r-- | lib/classes/cache/Item.php | 164 | ||||
| -rw-r--r-- | lib/classes/cache/KeyTrait.php | 31 | ||||
| -rw-r--r-- | lib/classes/cache/MemcachedCache.php | 151 | ||||
| -rw-r--r-- | lib/classes/cache/MemoryCache.php | 98 | ||||
| -rw-r--r-- | lib/classes/cache/Proxy.php | 123 | ||||
| -rw-r--r-- | lib/classes/cache/RedisCache.php | 198 | ||||
| -rw-r--r-- | lib/classes/cache/Wrapper.php | 95 |
13 files changed, 1755 insertions, 0 deletions
diff --git a/lib/classes/cache/Cache.php b/lib/classes/cache/Cache.php new file mode 100644 index 0000000..be703cf --- /dev/null +++ b/lib/classes/cache/Cache.php @@ -0,0 +1,213 @@ +<?php + +namespace Studip\Cache; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; + +/** + * An abstract class which has to be extended by instances returned from + * \Studip\Cache\Factory#getCache + * + * @author Marco Diedrich (mdiedric@uos) + * @author Marcus Lunzenauer (mlunzena@uos.de) + * @author Moritz Strohm <strohm@data-quest.de> + * @copyright (c) Authors + * @since 1.6 + * @license GPL2 or any later version + */ +abstract class Cache implements CacheItemPoolInterface +{ + const DEFAULT_EXPIRATION = 12 * 60 * 60; // 12 hours + + /** + * @return string A translateable display name for this cache class. + */ + abstract public static function getDisplayName(): string; + + /** + * Get some statistics from cache, like number of entries, hit rate or + * whatever the underlying cache provides. + * Results are returned in form of an array like + * "[ + * [ + * 'name' => <displayable name> + * 'value' => <value of the current stat> + * ] + * ]" + * + * @return array + */ + abstract public function getStats(): array; + + /** + * Return the Vue component name and props that handle configuration. + * The associative array is of the form + * [ + * 'component' => <Vue component name>, + * 'props' => <Properties for component> + * ] + * + * @return array + */ + abstract public static function getConfig(): array; + + /** + * Expire item from the cache. + * + * Example: + * + * # expires foo + * $cache->expire('foo'); + * + * @param string $arg a single key + */ + abstract public function expire($arg); + + /** + * Expire all items from the cache. + */ + abstract public function flush(); + + /** + * @see CacheItemPoolInterface::getItem + */ + abstract public function getItem(string $key): CacheItemInterface; + + /** + * @see CacheItemPoolInterface::hasItem + */ + abstract public function hasItem(string $key): bool; + + /** + * @var array An array of deferred items that shall be saved only + * when commit() is called. This is only used in PSR-6 cache methods. + */ + protected array $deferred_items = []; + + /** + * Retrieve item from the server. + * + * Example: + * + * # reads foo + * $foo = $cache->reads('foo'); + * + * @param string $arg a single key + * + * @return mixed the previously stored data if an item with such a key + * exists on the server or FALSE on failure. + * + * @deprecated To be removed with Stud.IP 7.0. + */ + public function read($arg) + { + $item = $this->getItem($arg); + if ($item->isHit()) { + return $item->get(); + } + return false; + } + + /** + * Store data at the server. + * + * @param string $name the item's key. + * @param mixed $content the item's content (will be serialized if necessary). + * @param int $expires the item's expiry time in seconds. Optional, defaults to 12h. + * + * @return bool returns TRUE on success or FALSE on failure. + + * @deprecated To be removed with Stud.IP 7.0. + */ + public function write($name, $content, $expires = self::DEFAULT_EXPIRATION) + { + $item = new Item($name, $content, $expires); + + return $this->save($item); + } + + /** + * Calculates the expiration by a cache item. If that cannot be determined, + * the default expiration period is returned. + * + * @param Item $item The item from which to get the expiration time. + * + * @return int The time from now until the expiration in seconds. + */ + public function getExpiration(CacheItemInterface $item) : int + { + $expiration = self::DEFAULT_EXPIRATION; + if ($item instanceof Item) { + $expiration = $item->getExpirationInSeconds(); + } + return $expiration; + } + + // PSR-6 CacheItemPoolInterface: + + /** + * @see CacheItemPoolInterface::getItems + */ + public function getItems(array $keys = []): iterable + { + $items = []; + foreach ($keys as $key) { + $item = $this->getItem($key); + if ($item instanceof Item) { + $items[] = $item; + } + } + return $items; + } + + /** + * @see CacheItemPoolInterface::clear + */ + public function clear(): bool + { + $this->deferred_items = []; + $this->flush(); + return true; + } + + /** + * @see CacheItemPoolInterface::deleteItem + */ + public function deleteItem($key): bool + { + $this->expire($key); + return true; + } + + /** + * @see CacheItemPoolInterface::deleteItems + */ + public function deleteItems(array $keys): bool + { + foreach ($keys as $key) { + $this->expire($key); + } + return true; + } + + /** + * @see CacheItemPoolInterface::saveDeferred + */ + public function saveDeferred(CacheItemInterface $item): bool + { + $this->deferred_items[] = $item; + return true; + } + + /** + * @see CacheItemPoolInterface::commit + */ + public function commit(): bool + { + foreach ($this->deferred_items as $item) { + $this->save($item); + } + return true; + } +} diff --git a/lib/classes/cache/DbCache.php b/lib/classes/cache/DbCache.php new file mode 100644 index 0000000..c7abef2 --- /dev/null +++ b/lib/classes/cache/DbCache.php @@ -0,0 +1,143 @@ +<?php + +namespace Studip\Cache; + +use DBManager; +use Psr\Cache\CacheItemInterface; + +/** + * StudipCache implementation using database table + * + * @author Elmar Ludwig <elmar.ludwig@uos.de> + */ +class DbCache extends Cache +{ + /** + * @return string A display name (that can be translated) for this cache class. + */ + public static function getDisplayName(): string + { + return _('Datenbank'); + } + + /** + * Expire item from the cache. + * + * @param string $arg a single key + */ + public function expire($arg) + { + $db = DBManager::get(); + + $stmt = $db->prepare('DELETE FROM cache WHERE cache_key = ?'); + $stmt->execute([$arg]); + } + + /** + * Expire all items from the cache. + */ + public function flush() + { + $db = DBManager::get(); + + $db->exec('TRUNCATE TABLE cache'); + } + + /** + * Delete all expired items from the cache. + */ + public function purge() + { + $db = DBManager::get(); + + $stmt = $db->prepare('DELETE FROM cache WHERE expires < ?'); + $stmt->execute([time()]); + } + + /** + * Return statistics. + * + * @return array|array[] + *@see Cache::getStats() + * + */ + public function getStats(): array + { + return [ + __CLASS__ => [ + 'name' => _('Anzahl Einträge'), + 'value' => DBManager::get()->fetchColumn("SELECT COUNT(*) FROM `cache`") + ] + ]; + } + + /** + * Return the Vue component name and props that handle configuration. + * + * @return array + *@see Cache::getConfig() + * + */ + public static function getConfig(): array + { + return [ + 'component' => null, + 'props' => [] + ]; + } + + /** + * @inheritDoc + */ + public function getItem(string $key): CacheItemInterface + { + $query = "SELECT `content`, `expires` + FROM `cache` + WHERE `cache_key` = :key + AND `expires` > UNIX_TIMESTAMP()"; + $result = DBManager::get()->fetchOne($query, [':key' => $key]); + + $item = new Item($key); + if (!empty($result)) { + $item->setHit(); + if ($result['content']) { + $item->set(unserialize($result['content'])); + } + if ($result['expires']) { + $expiration = new \DateTime(); + $expiration->setTimestamp($result['expires']); + $item->expiresAt($expiration); + } + } + return $item; + } + + /** + * @inheritDoc + */ + public function hasItem(string $key): bool + { + $query = "SELECT 1 + FROM `cache` + WHERE `cache_key` = :key + AND `expires` > UNIX_TIMESTAMP()"; + return (bool) DBManager::get()->fetchColumn($query, [':key' => $key]); + } + + /** + * @inheritDoc + */ + public function save(CacheItemInterface $item): bool + { + $expiration = $this->getExpiration($item); + if ($expiration < 1) { + // The item would expire immediately. + return false; + } + + return DBManager::get()->execute( + 'REPLACE INTO `cache` VALUES (?, ?, ?)', + [$item->getKey(), serialize($item->get()), $expiration] + ); + } +} diff --git a/lib/classes/cache/Exception.php b/lib/classes/cache/Exception.php new file mode 100644 index 0000000..061d090 --- /dev/null +++ b/lib/classes/cache/Exception.php @@ -0,0 +1,27 @@ +<?php +/* + * CacheException.php + * This file is part of Stud.IP. + * + * 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. + * + * @author Moritz Strohm <strohm@data-quest.de> + * @copyright 2024 + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + * @since 6.0 + */ + +namespace Studip\Cache; + +/** + * The CacheException class is an implementation of the CacheException interface + * of PSR-6 that behaves like a StudipException. + */ +class Exception extends \StudipException implements \Psr\Cache\CacheException +{ + //Nothing here, since there is nothing to implement. +} diff --git a/lib/classes/cache/Factory.php b/lib/classes/cache/Factory.php new file mode 100644 index 0000000..b5c8359 --- /dev/null +++ b/lib/classes/cache/Factory.php @@ -0,0 +1,206 @@ +<?php + +namespace Studip\Cache; + +use Config; +use DBSchemaVersion; +use MessageBox; +use PageLayout; +use ReflectionClass; +use StudipCacheOperation; +use UnexpectedValueException; + +/** + * This factory retrieves the instance of StudipCache configured for use in + * this Stud.IP installation. + * + * @package studip + * @subpackage lib + * + * @author Marco Diedrich (mdiedric@uos) + * @author Marcus Lunzenauer (mlunzena@uos.de) + * @copyright 2007 (c) Authors + * @since 1.6 + * @license GPL2 or any later version + */ +class Factory +{ + /** + * the default cache class + * + * @var string + */ + const DEFAULT_CACHE_CLASS = DbCache::class; + + /** + * singleton instance + * + * @var Cache|null + */ + private static ?Cache $cache = null; + + + /** + * config instance + * + * @var Config|null + */ + private static ?Config $config = null; + + + /** + * Returns the currently used config instance + * + * @return Config an instance of class Config used by this factory to + * determine the class of the actual implementation of + * the StudipCache interface; if no config was set, it + * returns the instance returned by Config#getInstance + * @see Config + */ + public static function getConfig() + { + return self::$config ?? Config::getInstance(); + } + + + /** + * @param Config $config an instance of class Config which will be used to + * determine the class of the implementation of interface + * StudipCache + */ + public static function setConfig(Config $config) + { + self::$config = $config; + self::$cache = NULL; + } + + /** + * Resets the configuration and voids the cache instance. + */ + public static function unconfigure() + { + self::$cache = NULL; + } + + /** + * Returns a cache instance. + * + * @param bool $apply_proxied_operations Whether or not to apply any + * proxied (disable this in tests!) + * @return Cache the cache instance + */ + public static function getCache(bool $apply_proxied_operations = true): Cache + { + if (self::$cache === null) { + $proxied = false; + + if (!$GLOBALS['CACHING_ENABLE']) { + self::$cache = new MemoryCache(); + + // Proxy cache operations if CACHING_ENABLE is different from the globally set + // caching value. This should only be the case in cli mode. + if (isset($GLOBALS['GLOBAL_CACHING_ENABLE']) && $GLOBALS['GLOBAL_CACHING_ENABLE']) { + $proxied = true; + } + } else { + try { + $class = self::loadCacheClass(); + $args = self::retrieveConstructorArguments(); + + self::$cache = self::instantiateCache($class, $args); + } catch (\Exception $e) { + error_log(__METHOD__ . ': ' . $e->getMessage()); + PageLayout::addBodyElements(MessageBox::error(__METHOD__ . ': ' . $e->getMessage())); + $class = self::DEFAULT_CACHE_CLASS; + self::$cache = new $class(); + } + } + + // If proxy should be used, inject it. Otherwise apply pending + // operations, if any. + if ($proxied) { + self::$cache = new Proxy(self::$cache); + } elseif ($GLOBALS['CACHING_ENABLE'] && $apply_proxied_operations) { + // Even if the above condition will try to eliminate most + // failures, the following operation still needs to be wrapped + // in a try/catch block. Otherwise there are no means to + // execute migration 166 which creates the neccessary tables + // for said operation. + try { + StudipCacheOperation::apply(self::$cache); + } catch (\Exception $e) { + } + } + } + + return self::$cache; + } + + + /** + * Load configured cache class and return its name. + * + * @return string the name of the configured cache class + */ + public static function loadCacheClass() + { + $cacheConfig = self::getConfig()->SYSTEMCACHE; + + $cache_class = $cacheConfig['type'] ?: null; + + # default class + if ($cache_class === null) { + $version = new DBSchemaVersion(); + if ($version->get(1) < 224) { + // db cache is not yet available, use StudipMemoryCache + return 'StudipMemoryCache'; + } + + return self::DEFAULT_CACHE_CLASS; + } + + if (!class_exists($cache_class)) { + throw new UnexpectedValueException("Could not find class: '$cache_class'"); + } + + return $cache_class; + } + + /** + * Return an array of arguments required for instantiation of the cache + * class. + * + * @return array the array of arguments + */ + public static function retrieveConstructorArguments() + { + $cacheConfig = self::getConfig()->SYSTEMCACHE; + + return $cacheConfig ?: []; + } + + /** + * Return an instance of a given class using some arguments. Unless the + * memory cache is instantiated, the cache will be wrapped in a wrapper + * class that uses a memory cache to reduce accesses to the cache. + * + * @param string $class the name of the class + * @param array $arguments an array of arguments to be used by the constructor + * + * @return Cache an instance of the specified class + * @throws \ReflectionException + */ + public static function instantiateCache($class, $arguments) + { + $reflection_class = new ReflectionClass($class); + $cache = (is_array($arguments['config']) && count($arguments['config']) > 0) + ? $reflection_class->newInstanceArgs($arguments['config']) + : $reflection_class->newInstance(); + + if ($class !== MemoryCache::class) { + return new Wrapper($cache); + } + + return $cache; + } +} diff --git a/lib/classes/cache/FileCache.php b/lib/classes/cache/FileCache.php new file mode 100644 index 0000000..d760f08 --- /dev/null +++ b/lib/classes/cache/FileCache.php @@ -0,0 +1,278 @@ +<?php + +namespace Studip\Cache; + +use Config; +use Exception; +use Psr\Cache\CacheItemInterface; + +/** + * Cache implementation using files + * + * @author André Noack <noack@data-quest.de> + * @copyright 2007 André Noack <noack@data-quest.de> + * @license GPL2 or any later version + */ +class FileCache extends Cache +{ + use KeyTrait; + + /** + * full path to cache directory + * + * @var string + */ + private string $dir; + + /** + * @return string A translateable display name for this cache class. + */ + public static function getDisplayName(): string + { + return _('Dateisystem'); + } + + /** + * without the 'dir' argument the cache path is taken from + * $CACHING_FILECACHE_PATH or is set to + * $TMP_PATH/studip_cache + * + * @param string $path the path to use + * @throws Exception if the directory does not exist or could not be + * created + */ + public function __construct(string $path = '') + { + $this->dir = $path + ?: ( + Config::get()->SYSTEMCACHE['type'] === self::class + ? Config::get()->SYSTEMCACHE['config']['path'] + : '' + ) + ?: $GLOBALS['CACHING_FILECACHE_PATH'] + ?: ($GLOBALS['TMP_PATH'] . '/' . 'studip_cache'); + $this->dir = rtrim($this->dir, '\\/') . '/'; + + if (!is_dir($this->dir) && !@mkdir($this->dir, 0700)) { + throw new \Exception('Could not create directory: ' . $this->dir); + } + + if (!is_writable($this->dir)) { + throw new \Exception('Can not write to directory: ' . $this->dir); + } + } + + /** + * get path to cache directory + * + * @return string + */ + public function getCacheDir() + { + return $this->dir; + } + + /** + * expire cache item + * + * @param string $arg + * + * @return void + * @throws Exception + * @see Cache::expire() + */ + public function expire($arg) + { + $key = $this->getCacheKey($arg); + + if ($file = $this->getPathAndFile($key)){ + @unlink($file); + } + } + + /** + * Expire all items from the cache. + */ + public function flush() + { + rmdirr($this->dir); + } + + /** + * checks if specified cache item is expired + * if expired the cache file is deleted + * + * @param string $key a cache key to check + * + * @return array|bool the path to the cache file or false if expired + * @throws Exception + */ + private function check($key) + { + if ($file = $this->getPathAndFile($key)){ + [$id, $expire] = explode('-', basename($file)); + if (time() < $expire) { + return [$file, $expire]; + } else { + @unlink($file); + } + } + return false; + } + + /** + * get the full path to a cache file + * + * the cache files are organized in sub-folders named by + * the first two characters of the hashed cache key. + * the filename is constructed from the hashed cache key + * and the timestamp of expiration + * + * @param string $key a cache key + * @param int|null $expire expiry time in seconds + * + * @return string|bool full path to cache item or false on failure + * @throws Exception + */ + private function getPathAndFile(string $key, ?int $expire = null): bool|string + { + $id = hash('md5', $key); + $path = $this->dir . mb_substr($id, 0, 2); + if (!is_dir($path) && !@mkdir($path, 0700)) { + throw new \Exception('Could not create directory: ' . $path); + } + if (!is_null($expire)){ + return $path . '/' . $id . '-' . (time() + $expire); + } else { + $files = @glob("{$path}/{$id}*"); + if (count($files) > 0) { + return $files[0]; + } + } + return false; + } + + /** + * purges expired entries from the cache directory + * + * @param bool $be_quiet echo messages if set to false + * + * @return int the number of deleted files + */ + public function purge(bool $be_quiet = true): int + { + $now = time(); + $deleted = 0; + foreach (@glob($this->dir . '*', GLOB_ONLYDIR) as $current_dir){ + foreach (@glob("{$current_dir}/*") as $file){ + [$id, $expire] = explode('-', basename($file)); + if ($expire < $now) { + if (@unlink($file)) { + ++$deleted; + if (!$be_quiet) { + echo "File: {$file} deleted.\n"; + } + } + } else if (!$be_quiet) { + echo "File: {$file} expires on " . date('Y-m-d H:i:s', $expire) . "\n"; + } + } + } + return $deleted; + } + + /** + * Return statistics. + * + * @return array|array[] + */ + public function getStats(): array + { + return [ + __CLASS__ => [ + 'name' => _('Anzahl Einträge'), + 'value' => \DBManager::get()->fetchColumn("SELECT COUNT(*) FROM `cache`") + ] + ]; + } + + /** + * Return the Vue component name and props that handle configuration. + * + * @return array + */ + public static function getConfig(): array + { + $currentCache = Config::get()->SYSTEMCACHE; + + // Set default config for this cache + $currentConfig = [ + 'path' => $GLOBALS['TMP_PATH'] . '/studip_cache' + ]; + + // If this cache is set as system cache, use config from global settings. + if ($currentCache['type'] == __CLASS__) { + $currentConfig = $currentCache['config']; + } + + return [ + 'component' => 'FileCacheConfig', + 'props' => $currentConfig + ]; + } + + /** + * @inheritDoc + */ + public function getItem(string $key): CacheItemInterface + { + $real_key = $this->getCacheKey($key); + + $item = new \Studip\Cache\Item($key); + + $file_data = $this->check($real_key); + if ($file_data) { + $file = $file_data[0]; + $expire = $file_data[1]; + $f = @fopen($file, 'rb'); + if ($f) { + @flock($f, LOCK_SH); + $result = stream_get_contents($f); + @fclose($f); + } + $item->setHit(); + $item->set(unserialize($result)); + $expiration = new \DateTime(); + $expiration->setTimestamp($expire); + $item->expiresAt($expiration); + } + return $item; + } + + /** + * @inheritDoc + */ + public function hasItem(string $key): bool + { + $real_key = $this->getCacheKey($key); + $file_data = $this->check($real_key); + return $file_data !== false; + } + + /** + * @inheritDoc + */ + public function save(CacheItemInterface $item): bool + { + $expiration = $this->getExpiration($item); + if ($expiration < 1) { + //The item would expire immediately. + return false; + } + + $real_key = $this->getCacheKey($item->getKey()); + $this->expire($real_key); + $file = $this->getPathAndFile($real_key, $expiration); + return @file_put_contents($file, serialize($item->get()), LOCK_EX); + } +} diff --git a/lib/classes/cache/InvalidCacheArgumentException.php b/lib/classes/cache/InvalidCacheArgumentException.php new file mode 100644 index 0000000..a201cad --- /dev/null +++ b/lib/classes/cache/InvalidCacheArgumentException.php @@ -0,0 +1,28 @@ +<?php +/* + * CacheException.php + * This file is part of Stud.IP. + * + * 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. + * + * @author Moritz Strohm <strohm@data-quest.de> + * @copyright 2024 + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + * @since 6.0 + */ + +namespace Studip\Cache; + + +/** + * The InvalidCacheArgumentException is an implementation of the InvalidArgumentException interface + * of PSR-6 that behaves like a StudipException. + */ +class InvalidCacheArgumentException extends \StudipException implements \Psr\Cache\InvalidArgumentException +{ + //Nothing here, since there is nothing to implement. +} diff --git a/lib/classes/cache/Item.php b/lib/classes/cache/Item.php new file mode 100644 index 0000000..99a6df8 --- /dev/null +++ b/lib/classes/cache/Item.php @@ -0,0 +1,164 @@ +<?php +/** + * Item.php + * This file is part of Stud.IP. + * + * 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. + * + * @author Moritz Strohm <strohm@data-quest.de> + * @copyright 2024 + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + * @since 6.0 + */ + +namespace Studip\Cache; + +use DateInterval; +use DateTime; +use Psr\Cache\CacheItemInterface; + +/** + * \Studip\Cache\CacheItem implements the CacheItemInterface of PSR-6. It holds the value and the + * key of a cache item and also provides additional methods to get the expiration of the item. + */ +class Item implements CacheItemInterface +{ + /** + * @var string The key of the item in the cache. + */ + protected string $key; + + /** + * @var mixed The value of the item. + */ + protected mixed $value; + + /** + * @var DateTime|null The expiration as DateTime object or null if the expiration is not defined. + */ + protected ?DateTime $expiration = null; + + /** + * @var bool An indicator whether the item has been found in the cache (true) or not (false). + */ + protected bool $cache_hit = false; + + /** + * The constructor of \Studip\Cache\CacheItem. + * + * @param string $key The key of the item in the cache. + * @param mixed $value The value of the item. + * @param int|null $expiration The expiration of the item in seconds, if applicable. + * @param bool $cache_hit Whether the item shall be constructed as cache hit (true) or not (false). + * + */ + public function __construct( + string $key, + mixed $value = null, + ?int $expiration = null, + bool $cache_hit = false + ) { + $this->key = $key; + $this->value = $value; + $this->cache_hit = $cache_hit; + $this->expiresAfter($expiration); + } + + /** + * @inheritDoc + */ + public function getKey(): string + { + return $this->key; + } + + /** + * @inheritDoc + */ + public function get(): mixed + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function isHit(): bool + { + return $this->cache_hit; + } + + /** + * @inheritDoc + */ + public function set($value): static + { + $this->value = $value; + return $this; + } + + /** + * @inheritDoc + */ + public function expiresAt($expiration): static + { + $this->expiration = $expiration; + return $this; + } + + /** + * @inheritDoc + */ + public function expiresAfter($time): static + { + $this->expiration = new DateTime(); + if ($time instanceof DateInterval) { + $this->expiration = $this->expiration->add($time); + } elseif (is_integer($time)) { + $this->expiration->setTimestamp(time() + $time); + } else { + $this->expiration->setTimestamp(time() + Cache::DEFAULT_EXPIRATION); + } + return $this; + } + + // \Studip\Cache\CacheItem specific methods: + + /** + * Sets the item to be a cache hit. + * + * @return void + */ + public function setHit() : void + { + $this->cache_hit = true; + } + + /** + * Returns the expiration, if set. + * + * @return DateTime|null A DateTime object with the expiration date and time + * or null if the expiration is not defined. + */ + public function getExpiration() : ?DateTime + { + return $this->expiration; + } + + /** + * Returns the seconds from the current timestamp until the expiration of the item. + * + * @return int The seconds until the item expires + */ + public function getExpirationInSeconds() : int + { + if ($this->expiration) { + return $this->expiration->getTimestamp() - time(); + } + return 0; + } +} diff --git a/lib/classes/cache/KeyTrait.php b/lib/classes/cache/KeyTrait.php new file mode 100644 index 0000000..021aaba --- /dev/null +++ b/lib/classes/cache/KeyTrait.php @@ -0,0 +1,31 @@ +<?php +namespace Studip\Cache; + +/** + * Trait for unique cache hashes per key for each system based on db configuration which should + * be sufficient to eliminate cache mishaps. + * + * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> + * @license GPL2 or any later version + * @package studip + * @subpackage cache + * @since Stud.IP 5.0 + */ +trait KeyTrait +{ + protected ?string $cache_prefix = null; + + /** + * Returns a prefix cache key based on db configuration. + * + * @param string $offset + * @return string + */ + protected function getCacheKey(string $offset): string + { + if ($this->cache_prefix === null) { + $this->cache_prefix = md5("{$GLOBALS['DB_STUDIP_HOST']}|{$GLOBALS['DB_STUDIP_DATABASE']}"); + } + return "$this->cache_prefix/$offset"; + } +} diff --git a/lib/classes/cache/MemcachedCache.php b/lib/classes/cache/MemcachedCache.php new file mode 100644 index 0000000..1c4b685 --- /dev/null +++ b/lib/classes/cache/MemcachedCache.php @@ -0,0 +1,151 @@ +<?php + +namespace Studip\Cache; + +use Memcached; +use Psr\Cache\CacheItemInterface; + +/** + * Cache implementation using memcached. + * + * @author Marcus Lunzenauer <mlunzena@uos.de> + * @copyright (c) Authors + * @license GPL2 or any later version + * @since 5.0 + */ +class MemcachedCache extends Cache +{ + use KeyTrait; + + private Memcached $memcache; + + /** + * @return string A translateable display name for this cache class. + */ + public static function getDisplayName(): string + { + return _('Memcached'); + } + + public function __construct($servers) + { + if (!extension_loaded('memcached')) { + throw new \Exception('Memcache extension missing.'); + } + + $prefix = \Config::get()->STUDIP_INSTALLATION_ID; + $this->memcache = new Memcached('studip' . ($prefix ? '-' . $prefix : '')); + + if (count($this->memcache->getServerList()) === 0) { + foreach ($servers as $server) { + $status = $this->memcache->addServer($server['hostname'], (int) $server['port']); + + if (!$status) { + throw new \Exception("Could not add server: {$server['hostname']} @ port {$server['port']}"); + } + } + } + } + + /** + * Expire item from the cache. + * + * Example: + * + * # expires foo + * $cache->expire('foo'); + * + * @param string $arg a single key. + * @returns void + */ + public function expire($arg) + { + $key = $this->getCacheKey($arg); + $this->memcache->delete($key); + } + + /** + * Expire all items from the cache. + */ + public function flush() + { + $this->memcache->flush(); + } + + /** + * Return statistics. + * + * @StudipCache::getStats() + * + * @return array|array[] + */ + public function getStats(): array + { + return $this->memcache->getStats(); + } + + /** + * Return the Vue component name and props that handle configuration. + * + * @see Cache::getConfig() + * + * @return array + */ + public static function getConfig(): array + { + $currentCache = \Config::get()->SYSTEMCACHE; + + // Set default config for this cache + $currentConfig = [ + 'servers' => [] + ]; + + // If this cache is set as system cache, use config from global settings. + if ($currentCache['type'] == __CLASS__) { + $currentConfig = $currentCache['config']; + } + + return [ + 'component' => 'MemcachedCacheConfig', + 'props' => $currentConfig + ]; + } + + /** + * @inheritDoc + */ + public function getItem(string $key): CacheItemInterface + { + $item = new Item($key); + $value = $this->memcache->get($this->getCacheKey($key)); + if ($this->memcache->getResultCode() !== Memcached::RES_NOTFOUND) { + // Set the value, even if it is the boolean value false: + $item->setHit(); + $item->set($value); + } + return $item; + } + + /** + * @inheritDoc + */ + public function hasItem(string $key): bool + { + return $this->memcache->checkKey($this->getCacheKey($key)); + } + + /** + * @inheritDoc + */ + public function save(CacheItemInterface $item): bool + { + $expiration = $this->getExpiration($item); + if ($expiration < 1) { + // The item would expire immediately. + return false; + } + + $real_key = $this->getCacheKey($item->getKey()); + return $this->memcache->set($real_key, $item->get(), $expiration); + } +} diff --git a/lib/classes/cache/MemoryCache.php b/lib/classes/cache/MemoryCache.php new file mode 100644 index 0000000..7c00753 --- /dev/null +++ b/lib/classes/cache/MemoryCache.php @@ -0,0 +1,98 @@ +<?php + +namespace Studip\Cache; + +use DateTime; +use Psr\Cache\CacheItemInterface; + +/** + * The php memory implementation of the StudipCache interface. + * + * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> + * @license GPL2 or any later version + * @since Stud.IP 5.0 + */ +class MemoryCache extends Cache +{ + protected array $memory_cache = []; + + /** + * Expires just a single key. + * + * @param string $arg the key + */ + public function expire($arg) + { + unset($this->memory_cache[$arg]); + } + + /** + * Expire all items from the cache. + */ + public function flush() + { + $this->memory_cache = []; + } + + public static function getDisplayName(): string + { + return 'Memory cache'; + } + + public function getStats(): array + { + return []; + } + + public static function getConfig(): array + { + return []; + } + + /** + * @inheritDoc + */ + public function getItem(string $key): CacheItemInterface + { + $item = new Item($key); + if (!isset($this->memory_cache[$key])) { + return $item; + } + if ($this->memory_cache[$key]['expires'] < time()) { + $this->expire($key); + return $item; + } + $item->setHit(); + $item->set($this->memory_cache[$key]['data']); + if (!empty($this->memory_cache[$key]['expires'])) { + $expiration = new DateTime(); + $expiration->setTimestamp($this->memory_cache[$key]['expires']); + $item->expiresAt($expiration); + } + return $item; + } + + /** + * @inheritDoc + */ + public function hasItem(string $key): bool + { + return isset($this->memory_cache[$key]) + && $this->memory_cache[$key]['expires'] < time(); + } + + /** + * @inheritDoc + */ + public function save(CacheItemInterface $item): bool + { + $expiration = $this->getExpiration($item); + + $this->memory_cache[$item->getKey()] = [ + 'expires' => $expiration + time(), + 'data' => $item->get(), + ]; + + return true; + } +} diff --git a/lib/classes/cache/Proxy.php b/lib/classes/cache/Proxy.php new file mode 100644 index 0000000..fef034d --- /dev/null +++ b/lib/classes/cache/Proxy.php @@ -0,0 +1,123 @@ +<?php + +namespace Studip\Cache; + +use Psr\Cache\CacheItemInterface; +use StudipCacheOperation; + +/** + * Proxies a StudipCache and stores the expire operation in the database. + * These operations are lateron applied to the cache they should have + * been applied to in the beginning. + * + * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> + * @license GPL2 or any later version + * @since Stud.IP 3.3 + */ +class Proxy extends Cache +{ + protected Cache $actual_cache; + protected array $proxy_these; + + /** + * @param Cache $cache The actual cache object + * @param mixed $proxy_these List of operations to proxy (should be an + * array but a space seperated string is also + * valid) + */ + public function __construct(Cache $cache, $proxy_these = ['expire']) + { + if (!is_array($proxy_these)) { + $proxy_these = words($proxy_these); + } + + $this->actual_cache = $cache; + $this->proxy_these = is_array($proxy_these) + ? $proxy_these + : words($proxy_these); + } + + /** + * Expires just a single key. + * + * @param string $arg The item's key + */ + public function expire($arg) + { + if (in_array('expire', $this->proxy_these)) { + try { + $operation = new StudipCacheOperation([$arg, 'expire']); + $operation->parameters = serialize([]); + $operation->store(); + } catch (\Exception) { + } + } + + return $this->actual_cache->expire($arg); + } + + /** + * Expire all items from the cache. + */ + public function flush() + { + if (in_array('flush', $this->proxy_these)) { + try { + $operation = new StudipCacheOperation(['', 'flush']); + $operation->parameters = serialize([]); + $operation->store(); + } catch (\Exception) { + } + } + + return $this->actual_cache->flush(); + } + + public static function getDisplayName(): string + { + return static::class; + } + + public function getStats(): array + { + return $this->actual_cache->getStats(); + } + + public static function getConfig(): array + { + return []; + } + + /** + * @inheritDoc + */ + public function getItem(string $key): CacheItemInterface + { + return $this->actual_cache->getItem($key); + } + + /** + * @inheritDoc + */ + public function hasItem(string $key): bool + { + return $this->actual_cache->hasItem($key); + } + + /** + * @inheritDoc + */ + public function save(CacheItemInterface $item): bool + { + if (in_array('save', $this->proxy_these)) { + try { + $operation = new StudipCacheOperation([$item->getKey(), 'save']); + $operation->parameters = serialize([$item]); + $operation->store(); + } catch (\Exception) { + } + } + + return $this->actual_cache->save($item); + } +} diff --git a/lib/classes/cache/RedisCache.php b/lib/classes/cache/RedisCache.php new file mode 100644 index 0000000..a78f751 --- /dev/null +++ b/lib/classes/cache/RedisCache.php @@ -0,0 +1,198 @@ +<?php + +namespace Studip\Cache; + +use BadMethodCallException; +use Config; +use DateTime; +use Exception; +use Psr\Cache\CacheItemInterface; +use Redis; +use RedisException; + +/** + * Cache implementation using redis. + * + * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> + * @license GPL2 or any later version + * @package studip + * @subpackage cache + * @since Stud.IP 5.0 + */ +class RedisCache extends Cache +{ + use KeyTrait; + + private $redis; + + /** + * @return string A translateable display name for this cache class. + */ + public static function getDisplayName(): string + { + return _('Redis'); + } + + /** + * Construct a cache instance. + * + * @param string $hostname Hostname of redis server + * @param int $port Port of redis server + * @param string $auth Optional auth token/password + * + * @throws RedisException + */ + public function __construct($hostname, $port, string $auth = '') + { + if (!extension_loaded('redis')) { + throw new Exception('Redis extension missing.'); + } + + $this->redis = new Redis(); + $status = $this->redis->connect($hostname, $port, 1); + + if (!$status) { + throw new Exception('Could not add cache.'); + } + + if ($auth !== '') { + $this->redis->auth($auth); + } + } + + /** + * Returns the instance of the redis server connection. + * + * @return Redis instance + */ + public function getRedis() + { + return $this->redis; + } + + /** + * Expire item from the cache. + * + * Example: + * + * # expires foo + * $cache->expire('foo'); + * + * @param string $arg a single key. + */ + public function expire($arg) + { + $key = $this->getCacheKey($arg); + $this->redis->unlink($key); + } + + /** + * Expire all items from the cache. + */ + public function flush() + { + $pattern = $this->getCacheKey('*'); + foreach ($this->redis->keys($pattern) as $key) { + $this->redis->unlink($key); + } + } + + /** + * @param string $method Method to call + * @param array $args Arguments to pass + * @return false|mixed + */ + public function __call($method, $args) + { + if (is_callable([$this->redis, $method])) { + return call_user_func_array([$this->redis, $method], $args); + } + throw new BadMethodCallException("Method {$method} does not exist"); + } + + /** + * Return statistics. + * + * @StudipCache::getStats() + * + * @return array|array[] + */ + public function getStats(): array + { + $stats = $this->redis->info(); + $stats['size'] = count($this->redis->keys($this->getCacheKey('*'))); + return ["{$this->redis->getHost()}:{$this->redis->getPort()}" => $stats]; + } + + /* + * Return the Vue component name and props that handle configuration. + * + * @see StudipCache::getConfig() + * + * @return array + */ + public static function getConfig(): array + { + $currentCache = Config::get()->SYSTEMCACHE; + + // Set default config for this cache + $currentConfig = [ + 'hostname' => '', + 'port' => null + ]; + + // If this cache is set as system cache, use config from global settings. + if ($currentCache['type'] == __CLASS__) { + $currentConfig = $currentCache['config']; + $currentConfig['port'] = $currentConfig['port'] ? (int) $currentConfig['port'] : null; + } + + return [ + 'component' => 'RedisCacheConfig', + 'props' => $currentConfig + ]; + } + + /** + * @inheritDoc + */ + public function getItem(string $key): CacheItemInterface + { + $item = new Item($key); + $real_key = $this->getCacheKey($key); + $result = $this->redis->get($real_key); + if ($result === null) { + return $item; + } + $item->setHit(); + $item->set(unserialize($result)); + $expiration = new DateTime(); + $expiration->setTimestamp($this->redis->expiretime($real_key)); + $item->expiresAt($expiration); + return $item; + } + + /** + * @inheritDoc + */ + public function hasItem(string $key): bool + { + $real_key = $this->getCacheKey($key); + return $this->redis->get($real_key) !== null; + } + + /** + * @inheritDoc + */ + public function save(CacheItemInterface $item): bool + { + $expiration = $this->getExpiration($item); + if ($expiration < 1) { + // The item would expire immediately. + return false; + } + + $real_key = $this->getCacheKey($item->getKey()); + return $this->redis->setEx($real_key, $expiration, serialize($item->get())); + } +} diff --git a/lib/classes/cache/Wrapper.php b/lib/classes/cache/Wrapper.php new file mode 100644 index 0000000..4e6342c --- /dev/null +++ b/lib/classes/cache/Wrapper.php @@ -0,0 +1,95 @@ +<?php + +namespace Studip\Cache; + +use Psr\Cache\CacheItemInterface; + +/** + * The cache wrapper wraps a memory cache around another cache. This should + * reduce the accesses to the actual cache. + * + * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> + * @license GPL2 or any later version + * @since Stud.IP 5.4 + */ +class Wrapper extends Cache +{ + protected Cache $actual_cache; + protected MemoryCache $memory_cache; + + public function __construct(Cache $actual_cache) + { + $this->actual_cache = $actual_cache; + $this->memory_cache = new MemoryCache(); + } + + /** + * @inheritdoc + */ + public function expire($arg) + { + $this->memory_cache->expire($arg); + $this->actual_cache->expire($arg); + } + + /** + * @inheritdoc + */ + public function flush() + { + $this->memory_cache->flush(); + $this->actual_cache->flush(); + } + + public static function getDisplayName(): string + { + return static::class; + } + + public function getStats(): array + { + return $this->actual_cache->getStats(); + } + + public static function getConfig(): array + { + return []; + } + + /** + * @inheritDoc + */ + public function getItem(string $key): CacheItemInterface + { + $cached = $this->memory_cache->getItem($key); + if ($cached->isHit()) { + return $cached; + } + + $cached = $this->actual_cache->getItem($key); + if ($cached->isHit()) { + $this->memory_cache->save($cached); + } + return $cached; + } + + /** + * @inheritDoc + */ + public function hasItem(string $key): bool + { + return $this->actual_cache->hasItem($key); + } + + /** + * @inheritDoc + */ + public function save(CacheItemInterface $item): bool + { + if ($this->actual_cache->save($item)) { + return $this->memory_cache->save($item); + } else { + return false; + } + } +} |
