aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/cache
diff options
context:
space:
mode:
authorPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
committerPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
commit4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch)
tree5c07151ae61276d334e88f6309c30d439a85c12e /lib/classes/cache
parentda0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff)
parent97a188592c679890a25c37ab78463add76a52ff7 (diff)
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/classes/cache')
-rw-r--r--lib/classes/cache/Cache.php213
-rw-r--r--lib/classes/cache/DbCache.php143
-rw-r--r--lib/classes/cache/Exception.php27
-rw-r--r--lib/classes/cache/Factory.php206
-rw-r--r--lib/classes/cache/FileCache.php278
-rw-r--r--lib/classes/cache/InvalidCacheArgumentException.php28
-rw-r--r--lib/classes/cache/Item.php164
-rw-r--r--lib/classes/cache/KeyTrait.php31
-rw-r--r--lib/classes/cache/MemcachedCache.php151
-rw-r--r--lib/classes/cache/MemoryCache.php98
-rw-r--r--lib/classes/cache/Proxy.php123
-rw-r--r--lib/classes/cache/RedisCache.php198
-rw-r--r--lib/classes/cache/Wrapper.php95
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;
+ }
+ }
+}