premier commit

This commit is contained in:
Angle
2026-01-16 16:29:06 +01:00
commit a58748f0cd
1082 changed files with 156212 additions and 0 deletions
@@ -0,0 +1,210 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache\Adapter;
use DateInterval;
use Grav\Framework\Cache\AbstractCache;
use Grav\Framework\Cache\CacheInterface;
use Grav\Framework\Cache\Exception\InvalidArgumentException;
use function count;
use function get_class;
/**
* Cache class for PSR-16 compatible "Simple Cache" implementation using chained cache adapters.
*
* @package Grav\Framework\Cache
*/
class ChainCache extends AbstractCache
{
/** @var CacheInterface[] */
protected $caches;
/** @var int */
protected $count;
/**
* Chain Cache constructor.
* @param array $caches
* @param null|int|DateInterval $defaultLifetime
* @throws InvalidArgumentException
*/
public function __construct(array $caches, $defaultLifetime = null)
{
try {
parent::__construct('', $defaultLifetime);
} catch (\Psr\SimpleCache\InvalidArgumentException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
if (!$caches) {
throw new InvalidArgumentException('At least one cache must be specified');
}
foreach ($caches as $cache) {
if (!$cache instanceof CacheInterface) {
throw new InvalidArgumentException(
sprintf(
"The class '%s' does not implement the '%s' interface",
get_class($cache),
CacheInterface::class
)
);
}
}
$this->caches = array_values($caches);
$this->count = count($caches);
}
/**
* @inheritdoc
*/
public function doGet($key, $miss)
{
foreach ($this->caches as $i => $cache) {
$value = $cache->doGet($key, $miss);
if ($value !== $miss) {
while (--$i >= 0) {
// Update all the previous caches with missing value.
$this->caches[$i]->doSet($key, $value, $this->getDefaultLifetime());
}
return $value;
}
}
return $miss;
}
/**
* @inheritdoc
*/
public function doSet($key, $value, $ttl)
{
$success = true;
$i = $this->count;
while ($i--) {
$success = $this->caches[$i]->doSet($key, $value, $ttl) && $success;
}
return $success;
}
/**
* @inheritdoc
*/
public function doDelete($key)
{
$success = true;
$i = $this->count;
while ($i--) {
$success = $this->caches[$i]->doDelete($key) && $success;
}
return $success;
}
/**
* @inheritdoc
*/
public function doClear()
{
$success = true;
$i = $this->count;
while ($i--) {
$success = $this->caches[$i]->doClear() && $success;
}
return $success;
}
/**
* @inheritdoc
*/
public function doGetMultiple($keys, $miss)
{
$list = [];
/**
* @var int $i
* @var CacheInterface $cache
*/
foreach ($this->caches as $i => $cache) {
$list[$i] = $cache->doGetMultiple($keys, $miss);
$keys = array_diff_key($keys, $list[$i]);
if (!$keys) {
break;
}
}
// Update all the previous caches with missing values.
$values = [];
/**
* @var int $i
* @var CacheInterface $items
*/
foreach (array_reverse($list) as $i => $items) {
$values += $items;
if ($i && $values) {
$this->caches[$i-1]->doSetMultiple($values, $this->getDefaultLifetime());
}
}
return $values;
}
/**
* @inheritdoc
*/
public function doSetMultiple($values, $ttl)
{
$success = true;
$i = $this->count;
while ($i--) {
$success = $this->caches[$i]->doSetMultiple($values, $ttl) && $success;
}
return $success;
}
/**
* @inheritdoc
*/
public function doDeleteMultiple($keys)
{
$success = true;
$i = $this->count;
while ($i--) {
$success = $this->caches[$i]->doDeleteMultiple($keys) && $success;
}
return $success;
}
/**
* @inheritdoc
*/
public function doHas($key)
{
foreach ($this->caches as $cache) {
if ($cache->doHas($key)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,118 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache\Adapter;
use DateInterval;
use Doctrine\Common\Cache\CacheProvider;
use Grav\Framework\Cache\AbstractCache;
use Grav\Framework\Cache\Exception\InvalidArgumentException;
/**
* Cache class for PSR-16 compatible "Simple Cache" implementation using Doctrine Cache backend.
* @package Grav\Framework\Cache
*/
class DoctrineCache extends AbstractCache
{
/** @var CacheProvider */
protected $driver;
/**
* Doctrine Cache constructor.
*
* @param CacheProvider $doctrineCache
* @param string $namespace
* @param null|int|DateInterval $defaultLifetime
* @throws InvalidArgumentException
*/
public function __construct(CacheProvider $doctrineCache, $namespace = '', $defaultLifetime = null)
{
// Do not use $namespace or $defaultLifetime directly, store them with constructor and fetch with methods.
try {
parent::__construct($namespace, $defaultLifetime);
} catch (\Psr\SimpleCache\InvalidArgumentException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
// Set namespace to Doctrine Cache provider if it was given.
$namespace = $this->getNamespace();
if ($namespace) {
$doctrineCache->setNamespace($namespace);
}
$this->driver = $doctrineCache;
}
/**
* @inheritdoc
*/
public function doGet($key, $miss)
{
$value = $this->driver->fetch($key);
// Doctrine cache does not differentiate between no result and cached 'false'. Make sure that we do.
return $value !== false || $this->driver->contains($key) ? $value : $miss;
}
/**
* @inheritdoc
*/
public function doSet($key, $value, $ttl)
{
return $this->driver->save($key, $value, (int) $ttl);
}
/**
* @inheritdoc
*/
public function doDelete($key)
{
return $this->driver->delete($key);
}
/**
* @inheritdoc
*/
public function doClear()
{
return $this->driver->deleteAll();
}
/**
* @inheritdoc
*/
public function doGetMultiple($keys, $miss)
{
return $this->driver->fetchMultiple($keys);
}
/**
* @inheritdoc
*/
public function doSetMultiple($values, $ttl)
{
return $this->driver->saveMultiple($values, (int) $ttl);
}
/**
* @inheritdoc
*/
public function doDeleteMultiple($keys)
{
return $this->driver->deleteMultiple($keys);
}
/**
* @inheritdoc
*/
public function doHas($key)
{
return $this->driver->contains($key);
}
}
@@ -0,0 +1,266 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache\Adapter;
use ErrorException;
use FilesystemIterator;
use Grav\Framework\Cache\AbstractCache;
use Grav\Framework\Cache\Exception\CacheException;
use Grav\Framework\Cache\Exception\InvalidArgumentException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RuntimeException;
use function strlen;
/**
* Cache class for PSR-16 compatible "Simple Cache" implementation using file backend.
*
* Defaults to 1 year TTL. Does not support unlimited TTL.
*
* @package Grav\Framework\Cache
*/
class FileCache extends AbstractCache
{
/** @var string */
private $directory;
/** @var string|null */
private $tmp;
/**
* FileCache constructor.
* @param string $namespace
* @param int|null $defaultLifetime
* @param string|null $folder
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
public function __construct($namespace = '', $defaultLifetime = null, $folder = null)
{
try {
parent::__construct($namespace, $defaultLifetime ?: 31557600); // = 1 year
$this->initFileCache($namespace, $folder ?? '');
} catch (\Psr\SimpleCache\InvalidArgumentException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* @inheritdoc
*/
public function doGet($key, $miss)
{
$now = time();
$file = $this->getFile($key);
if (!file_exists($file) || !$h = @fopen($file, 'rb')) {
return $miss;
}
if ($now >= (int) $expiresAt = fgets($h)) {
fclose($h);
@unlink($file);
} else {
$i = rawurldecode(rtrim((string)fgets($h)));
$value = stream_get_contents($h) ?: '';
fclose($h);
if ($i === $key) {
return unserialize($value, ['allowed_classes' => true]);
}
}
return $miss;
}
/**
* @inheritdoc
* @throws CacheException
*/
public function doSet($key, $value, $ttl)
{
$expiresAt = time() + (int)$ttl;
$result = $this->write(
$this->getFile($key, true),
$expiresAt . "\n" . rawurlencode($key) . "\n" . serialize($value),
$expiresAt
);
if (!$result && !is_writable($this->directory)) {
throw new CacheException(sprintf('Cache directory is not writable (%s)', $this->directory));
}
return $result;
}
/**
* @inheritdoc
*/
public function doDelete($key)
{
$file = $this->getFile($key);
$result = false;
if (file_exists($file)) {
$result = @unlink($file);
$result &= !file_exists($file);
}
return $result;
}
/**
* @inheritdoc
*/
public function doClear()
{
$result = true;
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->directory, FilesystemIterator::SKIP_DOTS));
foreach ($iterator as $file) {
$result = ($file->isDir() || @unlink($file) || !file_exists($file)) && $result;
}
return $result;
}
/**
* @inheritdoc
*/
public function doHas($key)
{
$file = $this->getFile($key);
return file_exists($file) && (@filemtime($file) > time() || $this->doGet($key, null));
}
/**
* @param string $key
* @param bool $mkdir
* @return string
*/
protected function getFile($key, $mkdir = false)
{
$hash = str_replace('/', '-', base64_encode(hash('sha256', static::class . $key, true)));
$dir = $this->directory . $hash[0] . DIRECTORY_SEPARATOR . $hash[1] . DIRECTORY_SEPARATOR;
if ($mkdir) {
$this->mkdir($dir);
}
return $dir . substr($hash, 2, 20);
}
/**
* @param string $namespace
* @param string $directory
* @return void
* @throws InvalidArgumentException
*/
protected function initFileCache($namespace, $directory)
{
if ($directory === '') {
$directory = sys_get_temp_dir() . '/grav-cache';
} else {
$directory = realpath($directory) ?: $directory;
}
if (isset($namespace[0])) {
if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
}
$directory .= DIRECTORY_SEPARATOR . $namespace;
}
$this->mkdir($directory);
$directory .= DIRECTORY_SEPARATOR;
// On Windows the whole path is limited to 258 chars
if ('\\' === DIRECTORY_SEPARATOR && strlen($directory) > 234) {
throw new InvalidArgumentException(sprintf('Cache folder is too long (%s)', $directory));
}
$this->directory = $directory;
}
/**
* @param string $file
* @param string $data
* @param int|null $expiresAt
* @return bool
*/
private function write($file, $data, $expiresAt = null)
{
set_error_handler(__CLASS__.'::throwError');
try {
if ($this->tmp === null) {
$this->tmp = $this->directory . uniqid('', true);
}
file_put_contents($this->tmp, $data);
if ($expiresAt !== null) {
touch($this->tmp, $expiresAt);
}
return rename($this->tmp, $file);
} finally {
restore_error_handler();
}
}
/**
* @param string $dir
* @return void
* @throws RuntimeException
*/
private function mkdir($dir)
{
// Silence error for open_basedir; should fail in mkdir instead.
if (@is_dir($dir)) {
return;
}
$success = @mkdir($dir, 0777, true);
if (!$success) {
// Take yet another look, make sure that the folder doesn't exist.
clearstatcache(true, $dir);
if (!@is_dir($dir)) {
throw new RuntimeException(sprintf('Unable to create directory: %s', $dir));
}
}
}
/**
* @param int $type
* @param string $message
* @param string $file
* @param int $line
* @return bool
* @internal
* @throws ErrorException
*/
public static function throwError($type, $message, $file, $line)
{
throw new ErrorException($message, 0, $type, $file, $line);
}
/**
* @return void
*/
#[\ReturnTypeWillChange]
public function __destruct()
{
if ($this->tmp !== null && file_exists($this->tmp)) {
unlink($this->tmp);
}
}
}
@@ -0,0 +1,83 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache\Adapter;
use Grav\Framework\Cache\AbstractCache;
use function array_key_exists;
/**
* Cache class for PSR-16 compatible "Simple Cache" implementation using in memory backend.
*
* Memory backend does not use namespace or default ttl as the cache is unique to each cache object and request.
*
* @package Grav\Framework\Cache
*/
class MemoryCache extends AbstractCache
{
/** @var array */
protected $cache = [];
/**
* @param string $key
* @param mixed $miss
* @return mixed
*/
public function doGet($key, $miss)
{
if (!array_key_exists($key, $this->cache)) {
return $miss;
}
return $this->cache[$key];
}
/**
* @param string $key
* @param mixed $value
* @param int $ttl
* @return bool
*/
public function doSet($key, $value, $ttl)
{
$this->cache[$key] = $value;
return true;
}
/**
* @param string $key
* @return bool
*/
public function doDelete($key)
{
unset($this->cache[$key]);
return true;
}
/**
* @return bool
*/
public function doClear()
{
$this->cache = [];
return true;
}
/**
* @param string $key
* @return bool
*/
public function doHas($key)
{
return array_key_exists($key, $this->cache);
}
}
@@ -0,0 +1,107 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache\Adapter;
use Grav\Framework\Cache\AbstractCache;
/**
* Cache class for PSR-16 compatible "Simple Cache" implementation using session backend.
*
* @package Grav\Framework\Cache
*/
class SessionCache extends AbstractCache
{
public const VALUE = 0;
public const LIFETIME = 1;
/**
* @param string $key
* @param mixed $miss
* @return mixed
*/
public function doGet($key, $miss)
{
$stored = $this->doGetStored($key);
return $stored ? $stored[self::VALUE] : $miss;
}
/**
* @param string $key
* @param mixed $value
* @param int $ttl
* @return bool
*/
public function doSet($key, $value, $ttl)
{
$stored = [self::VALUE => $value];
if (null !== $ttl) {
$stored[self::LIFETIME] = time() + $ttl;
}
$_SESSION[$this->getNamespace()][$key] = $stored;
return true;
}
/**
* @param string $key
* @return bool
*/
public function doDelete($key)
{
unset($_SESSION[$this->getNamespace()][$key]);
return true;
}
/**
* @return bool
*/
public function doClear()
{
unset($_SESSION[$this->getNamespace()]);
return true;
}
/**
* @param string $key
* @return bool
*/
public function doHas($key)
{
return $this->doGetStored($key) !== null;
}
/**
* @return string
*/
public function getNamespace()
{
return 'cache-' . parent::getNamespace();
}
/**
* @param string $key
* @return mixed|null
*/
protected function doGetStored($key)
{
$stored = $_SESSION[$this->getNamespace()][$key] ?? null;
if (isset($stored[self::LIFETIME]) && $stored[self::LIFETIME] < time()) {
unset($_SESSION[$this->getNamespace()][$key]);
$stored = null;
}
return $stored ?: null;
}
}