mirror of
https://github.com/vichan-devel/vichan.git
synced 2024-11-27 17:00:52 +01:00
Merge pull request #818 from Zankaria/dep-inj-cache-wrap
Dependency injected cache
This commit is contained in:
commit
c1307feeb5
28
inc/Data/Driver/ApcuCacheDriver.php
Normal file
28
inc/Data/Driver/ApcuCacheDriver.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
namespace Vichan\Data\Driver;
|
||||||
|
|
||||||
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
|
|
||||||
|
class ApcuCacheDriver implements CacheDriver {
|
||||||
|
public function get(string $key): mixed {
|
||||||
|
$success = false;
|
||||||
|
$ret = \apcu_fetch($key, $success);
|
||||||
|
if ($success === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||||
|
\apcu_store($key, $value, (int)$expires);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $key): void {
|
||||||
|
\apcu_delete($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void {
|
||||||
|
\apcu_clear_cache();
|
||||||
|
}
|
||||||
|
}
|
28
inc/Data/Driver/ArrayCacheDriver.php
Normal file
28
inc/Data/Driver/ArrayCacheDriver.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
namespace Vichan\Data\Driver;
|
||||||
|
|
||||||
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple process-wide PHP array.
|
||||||
|
*/
|
||||||
|
class ArrayCacheDriver implements CacheDriver {
|
||||||
|
private static array $inner = [];
|
||||||
|
|
||||||
|
public function get(string $key): mixed {
|
||||||
|
return isset(self::$inner[$key]) ? self::$inner[$key] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||||
|
self::$inner[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $key): void {
|
||||||
|
unset(self::$inner[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void {
|
||||||
|
self::$inner = [];
|
||||||
|
}
|
||||||
|
}
|
38
inc/Data/Driver/CacheDriver.php
Normal file
38
inc/Data/Driver/CacheDriver.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
namespace Vichan\Data\Driver;
|
||||||
|
|
||||||
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
|
|
||||||
|
interface CacheDriver {
|
||||||
|
/**
|
||||||
|
* Get the value of associated with the key.
|
||||||
|
*
|
||||||
|
* @param string $key The key of the value.
|
||||||
|
* @return mixed|null The value associated with the key, or null if there is none.
|
||||||
|
*/
|
||||||
|
public function get(string $key): mixed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a key-value pair.
|
||||||
|
*
|
||||||
|
* @param string $key The key.
|
||||||
|
* @param mixed $value The value.
|
||||||
|
* @param int|false $expires After how many seconds the pair will expire. Use false or ignore this parameter to keep
|
||||||
|
* the value until it gets evicted to make space for more items. Some drivers will always
|
||||||
|
* ignore this parameter and store the pair until it's removed.
|
||||||
|
*/
|
||||||
|
public function set(string $key, mixed $value, mixed $expires = false): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a key-value pair.
|
||||||
|
*
|
||||||
|
* @param string $key The key.
|
||||||
|
*/
|
||||||
|
public function delete(string $key): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all the key-value pairs.
|
||||||
|
*/
|
||||||
|
public function flush(): void;
|
||||||
|
}
|
155
inc/Data/Driver/FsCachedriver.php
Normal file
155
inc/Data/Driver/FsCachedriver.php
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
namespace Vichan\Data\Driver;
|
||||||
|
|
||||||
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
|
|
||||||
|
class FsCacheDriver implements CacheDriver {
|
||||||
|
private string $prefix;
|
||||||
|
private string $base_path;
|
||||||
|
private mixed $lock_fd;
|
||||||
|
private int|false $collect_chance_den;
|
||||||
|
|
||||||
|
|
||||||
|
private function prepareKey(string $key): string {
|
||||||
|
$key = \str_replace('/', '::', $key);
|
||||||
|
$key = \str_replace("\0", '', $key);
|
||||||
|
return $this->prefix . $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sharedLockCache(): void {
|
||||||
|
\flock($this->lock_fd, LOCK_SH);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function exclusiveLockCache(): void {
|
||||||
|
\flock($this->lock_fd, LOCK_EX);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function unlockCache(): void {
|
||||||
|
\flock($this->lock_fd, LOCK_UN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function collectImpl(): int {
|
||||||
|
/*
|
||||||
|
* A read lock is ok, since it's alright if we delete expired items from under the feet of other processes, and
|
||||||
|
* no other process add new cache items or refresh existing ones.
|
||||||
|
*/
|
||||||
|
$files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT);
|
||||||
|
$count = 0;
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$data = \file_get_contents($file);
|
||||||
|
$wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR);
|
||||||
|
if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) {
|
||||||
|
if (@\unlink($file)) {
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function maybeCollect(): void {
|
||||||
|
if ($this->collect_chance_den !== false && \mt_rand(0, $this->collect_chance_den - 1) === 0) {
|
||||||
|
$this->collect_chance_den = false; // Collect only once per instance (aka process).
|
||||||
|
$this->collectImpl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct(string $prefix, string $base_path, string $lock_file, int|false $collect_chance_den) {
|
||||||
|
if ($base_path[\strlen($base_path) - 1] !== '/') {
|
||||||
|
$base_path = "$base_path/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\is_dir($base_path)) {
|
||||||
|
throw new \RuntimeException("$base_path is not a directory!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\is_writable($base_path)) {
|
||||||
|
throw new \RuntimeException("$base_path is not writable!");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->lock_fd = \fopen($base_path . $lock_file, 'w');
|
||||||
|
if ($this->lock_fd === false) {
|
||||||
|
throw new \RuntimeException('Unable to open the lock file!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->prefix = $prefix;
|
||||||
|
$this->base_path = $base_path;
|
||||||
|
$this->collect_chance_den = $collect_chance_den;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct() {
|
||||||
|
$this->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key): mixed {
|
||||||
|
$key = $this->prepareKey($key);
|
||||||
|
|
||||||
|
$this->sharedLockCache();
|
||||||
|
|
||||||
|
// Collect expired items first so if the target key is expired we shortcut to failure in the next lines.
|
||||||
|
$this->maybeCollect();
|
||||||
|
|
||||||
|
$fd = \fopen($this->base_path . $key, 'r');
|
||||||
|
if ($fd === false) {
|
||||||
|
$this->unlockCache();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = \stream_get_contents($fd);
|
||||||
|
\fclose($fd);
|
||||||
|
$this->unlockCache();
|
||||||
|
$wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) {
|
||||||
|
// Already expired, leave it there since we already released the lock and pretend it doesn't exist.
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return $wrapped['inner'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||||
|
$key = $this->prepareKey($key);
|
||||||
|
|
||||||
|
$wrapped = [
|
||||||
|
'expires' => $expires ? \time() + $expires : false,
|
||||||
|
'inner' => $value
|
||||||
|
];
|
||||||
|
|
||||||
|
$data = \json_encode($wrapped);
|
||||||
|
$this->exclusiveLockCache();
|
||||||
|
$this->maybeCollect();
|
||||||
|
\file_put_contents($this->base_path . $key, $data);
|
||||||
|
$this->unlockCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $key): void {
|
||||||
|
$key = $this->prepareKey($key);
|
||||||
|
|
||||||
|
$this->exclusiveLockCache();
|
||||||
|
@\unlink($this->base_path . $key);
|
||||||
|
$this->maybeCollect();
|
||||||
|
$this->unlockCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collect(): int {
|
||||||
|
$this->sharedLockCache();
|
||||||
|
$count = $this->collectImpl();
|
||||||
|
$this->unlockCache();
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void {
|
||||||
|
$this->exclusiveLockCache();
|
||||||
|
$files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
@\unlink($file);
|
||||||
|
}
|
||||||
|
$this->unlockCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close(): void {
|
||||||
|
\fclose($this->lock_fd);
|
||||||
|
}
|
||||||
|
}
|
43
inc/Data/Driver/MemcacheCacheDriver.php
Normal file
43
inc/Data/Driver/MemcacheCacheDriver.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
namespace Vichan\Data\Driver;
|
||||||
|
|
||||||
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
|
|
||||||
|
class MemcachedCacheDriver implements CacheDriver {
|
||||||
|
private \Memcached $inner;
|
||||||
|
|
||||||
|
public function __construct(string $prefix, string $memcached_server) {
|
||||||
|
$this->inner = new \Memcached();
|
||||||
|
if (!$this->inner->setOption(\Memcached::OPT_BINARY_PROTOCOL, true)) {
|
||||||
|
throw new \RuntimeException('Unable to set the memcached protocol!');
|
||||||
|
}
|
||||||
|
if (!$this->inner->setOption(\Memcached::OPT_PREFIX_KEY, $prefix)) {
|
||||||
|
throw new \RuntimeException('Unable to set the memcached prefix!');
|
||||||
|
}
|
||||||
|
if (!$this->inner->addServers($memcached_server)) {
|
||||||
|
throw new \RuntimeException('Unable to add the memcached server!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key): mixed {
|
||||||
|
$ret = $this->inner->get($key);
|
||||||
|
// If the returned value is false but the retrival was a success, then the value stored was a boolean false.
|
||||||
|
if ($ret === false && $this->inner->getResultCode() !== \Memcached::RES_SUCCESS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||||
|
$this->inner->set($key, $value, (int)$expires);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $key): void {
|
||||||
|
$this->inner->delete($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void {
|
||||||
|
$this->inner->flush();
|
||||||
|
}
|
||||||
|
}
|
26
inc/Data/Driver/NoneCacheDriver.php
Normal file
26
inc/Data/Driver/NoneCacheDriver.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
namespace Vichan\Data\Driver;
|
||||||
|
|
||||||
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op cache. Useful for testing.
|
||||||
|
*/
|
||||||
|
class NoneCacheDriver implements CacheDriver {
|
||||||
|
public function get(string $key): mixed {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||||
|
// No-op.
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $key): void {
|
||||||
|
// No-op.
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void {
|
||||||
|
// No-op.
|
||||||
|
}
|
||||||
|
}
|
48
inc/Data/Driver/RedisCacheDriver.php
Normal file
48
inc/Data/Driver/RedisCacheDriver.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
namespace Vichan\Data\Driver;
|
||||||
|
|
||||||
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
|
|
||||||
|
class RedisCacheDriver implements CacheDriver {
|
||||||
|
private string $prefix;
|
||||||
|
private \Redis $inner;
|
||||||
|
|
||||||
|
public function __construct(string $prefix, string $host, int $port, ?string $password, string $database) {
|
||||||
|
$this->inner = new \Redis();
|
||||||
|
$this->inner->connect($host, $port);
|
||||||
|
if ($password) {
|
||||||
|
$this->inner->auth($password);
|
||||||
|
}
|
||||||
|
if (!$this->inner->select($database)) {
|
||||||
|
throw new \RuntimeException('Unable to connect to Redis!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$$this->prefix = $prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key): mixed {
|
||||||
|
$ret = $this->inner->get($this->prefix . $key);
|
||||||
|
if ($ret === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return \json_decode($ret, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $key, mixed $value, mixed $expires = false): void {
|
||||||
|
if ($expires === false) {
|
||||||
|
$this->inner->set($this->prefix . $key, \json_encode($value));
|
||||||
|
} else {
|
||||||
|
$expires = $expires * 1000; // Seconds to milliseconds.
|
||||||
|
$this->inner->setex($this->prefix . $key, $expires, \json_encode($value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $key): void {
|
||||||
|
$this->inner->del($this->prefix . $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void {
|
||||||
|
$this->inner->flushDB();
|
||||||
|
}
|
||||||
|
}
|
177
inc/cache.php
177
inc/cache.php
@ -4,164 +4,89 @@
|
|||||||
* Copyright (c) 2010-2013 Tinyboard Development Group
|
* Copyright (c) 2010-2013 Tinyboard Development Group
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use Vichan\Data\Driver\{CacheDriver, ApcuCacheDriver, ArrayCacheDriver, FsCacheDriver, MemcachedCacheDriver, NoneCacheDriver, RedisCacheDriver};
|
||||||
|
|
||||||
defined('TINYBOARD') or exit;
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
|
|
||||||
class Cache {
|
class Cache {
|
||||||
private static $cache;
|
private static function buildCache(): CacheDriver {
|
||||||
public static function init() {
|
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
switch ($config['cache']['enabled']) {
|
switch ($config['cache']['enabled']) {
|
||||||
case 'memcached':
|
case 'memcached':
|
||||||
self::$cache = new Memcached();
|
return new MemcachedCacheDriver(
|
||||||
self::$cache->addServers($config['cache']['memcached']);
|
$config['cache']['prefix'],
|
||||||
break;
|
$config['cache']['memcached']
|
||||||
|
);
|
||||||
case 'redis':
|
case 'redis':
|
||||||
self::$cache = new Redis();
|
return new RedisCacheDriver(
|
||||||
self::$cache->connect($config['cache']['redis'][0], $config['cache']['redis'][1]);
|
$config['cache']['prefix'],
|
||||||
if ($config['cache']['redis'][2]) {
|
$config['cache']['redis'][0],
|
||||||
self::$cache->auth($config['cache']['redis'][2]);
|
$config['cache']['redis'][1],
|
||||||
}
|
$config['cache']['redis'][2],
|
||||||
self::$cache->select($config['cache']['redis'][3]) or die('cache select failure');
|
$config['cache']['redis'][3]
|
||||||
break;
|
);
|
||||||
|
case 'apcu':
|
||||||
|
return new ApcuCacheDriver;
|
||||||
|
case 'fs':
|
||||||
|
return new FsCacheDriver(
|
||||||
|
$config['cache']['prefix'],
|
||||||
|
"tmp/cache/{$config['cache']['prefix']}",
|
||||||
|
'.lock',
|
||||||
|
$config['auto_maintenance'] ? 1000 : false
|
||||||
|
);
|
||||||
|
case 'none':
|
||||||
|
return new NoneCacheDriver();
|
||||||
case 'php':
|
case 'php':
|
||||||
self::$cache = array();
|
default:
|
||||||
break;
|
return new ArrayCacheDriver();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getCache(): CacheDriver {
|
||||||
|
static $cache;
|
||||||
|
return $cache ??= self::buildCache();
|
||||||
|
}
|
||||||
|
|
||||||
public static function get($key) {
|
public static function get($key) {
|
||||||
global $config, $debug;
|
global $config, $debug;
|
||||||
|
|
||||||
$key = $config['cache']['prefix'] . $key;
|
$ret = self::getCache()->get($key);
|
||||||
|
if ($ret === null) {
|
||||||
$data = false;
|
$ret = false;
|
||||||
switch ($config['cache']['enabled']) {
|
|
||||||
case 'memcached':
|
|
||||||
if (!self::$cache)
|
|
||||||
self::init();
|
|
||||||
$data = self::$cache->get($key);
|
|
||||||
break;
|
|
||||||
case 'apcu':
|
|
||||||
$data = apcu_fetch($key);
|
|
||||||
break;
|
|
||||||
case 'php':
|
|
||||||
$data = isset(self::$cache[$key]) ? self::$cache[$key] : false;
|
|
||||||
break;
|
|
||||||
case 'fs':
|
|
||||||
$key = str_replace('/', '::', $key);
|
|
||||||
$key = str_replace("\0", '', $key);
|
|
||||||
if (!file_exists('tmp/cache/'.$key)) {
|
|
||||||
$data = false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$data = file_get_contents('tmp/cache/'.$key);
|
|
||||||
$data = json_decode($data, true);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'redis':
|
|
||||||
if (!self::$cache)
|
|
||||||
self::init();
|
|
||||||
$data = json_decode(self::$cache->get($key), true);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($config['debug'])
|
if ($config['debug']) {
|
||||||
$debug['cached'][] = $key . ($data === false ? ' (miss)' : ' (hit)');
|
$debug['cached'][] = $config['cache']['prefix'] . $key . ($ret === false ? ' (miss)' : ' (hit)');
|
||||||
|
}
|
||||||
|
|
||||||
return $data;
|
return $ret;
|
||||||
}
|
}
|
||||||
public static function set($key, $value, $expires = false) {
|
public static function set($key, $value, $expires = false) {
|
||||||
global $config, $debug;
|
global $config, $debug;
|
||||||
|
|
||||||
$key = $config['cache']['prefix'] . $key;
|
if (!$expires) {
|
||||||
|
|
||||||
if (!$expires)
|
|
||||||
$expires = $config['cache']['timeout'];
|
$expires = $config['cache']['timeout'];
|
||||||
|
|
||||||
switch ($config['cache']['enabled']) {
|
|
||||||
case 'memcached':
|
|
||||||
if (!self::$cache)
|
|
||||||
self::init();
|
|
||||||
self::$cache->set($key, $value, $expires);
|
|
||||||
break;
|
|
||||||
case 'redis':
|
|
||||||
if (!self::$cache)
|
|
||||||
self::init();
|
|
||||||
self::$cache->setex($key, $expires, json_encode($value));
|
|
||||||
break;
|
|
||||||
case 'apcu':
|
|
||||||
apcu_store($key, $value, $expires);
|
|
||||||
break;
|
|
||||||
case 'fs':
|
|
||||||
$key = str_replace('/', '::', $key);
|
|
||||||
$key = str_replace("\0", '', $key);
|
|
||||||
file_put_contents('tmp/cache/'.$key, json_encode($value));
|
|
||||||
break;
|
|
||||||
case 'php':
|
|
||||||
self::$cache[$key] = $value;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($config['debug'])
|
self::getCache()->set($key, $value, $expires);
|
||||||
$debug['cached'][] = $key . ' (set)';
|
|
||||||
|
if ($config['debug']) {
|
||||||
|
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (set)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public static function delete($key) {
|
public static function delete($key) {
|
||||||
global $config, $debug;
|
global $config, $debug;
|
||||||
|
|
||||||
$key = $config['cache']['prefix'] . $key;
|
self::getCache()->delete($key);
|
||||||
|
|
||||||
switch ($config['cache']['enabled']) {
|
if ($config['debug']) {
|
||||||
case 'memcached':
|
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (deleted)';
|
||||||
if (!self::$cache)
|
|
||||||
self::init();
|
|
||||||
self::$cache->delete($key);
|
|
||||||
break;
|
|
||||||
case 'redis':
|
|
||||||
if (!self::$cache)
|
|
||||||
self::init();
|
|
||||||
self::$cache->del($key);
|
|
||||||
break;
|
|
||||||
case 'apcu':
|
|
||||||
apcu_delete($key);
|
|
||||||
break;
|
|
||||||
case 'fs':
|
|
||||||
$key = str_replace('/', '::', $key);
|
|
||||||
$key = str_replace("\0", '', $key);
|
|
||||||
@unlink('tmp/cache/'.$key);
|
|
||||||
break;
|
|
||||||
case 'php':
|
|
||||||
unset(self::$cache[$key]);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($config['debug'])
|
|
||||||
$debug['cached'][] = $key . ' (deleted)';
|
|
||||||
}
|
}
|
||||||
public static function flush() {
|
public static function flush() {
|
||||||
global $config;
|
self::getCache()->flush();
|
||||||
|
|
||||||
switch ($config['cache']['enabled']) {
|
|
||||||
case 'memcached':
|
|
||||||
if (!self::$cache)
|
|
||||||
self::init();
|
|
||||||
return self::$cache->flush();
|
|
||||||
case 'apcu':
|
|
||||||
return apcu_clear_cache('user');
|
|
||||||
case 'php':
|
|
||||||
self::$cache = array();
|
|
||||||
break;
|
|
||||||
case 'fs':
|
|
||||||
$files = glob('tmp/cache/*');
|
|
||||||
foreach ($files as $file) {
|
|
||||||
unlink($file);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'redis':
|
|
||||||
if (!self::$cache)
|
|
||||||
self::init();
|
|
||||||
return self::$cache->flushDB();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,17 +139,26 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* On top of the static file caching system, you can enable the additional caching system which is
|
* On top of the static file caching system, you can enable the additional caching system which is
|
||||||
* designed to minimize SQL queries and can significantly increase speed when posting or using the
|
* designed to minimize request processing can significantly increase speed when posting or using
|
||||||
* moderator interface. APC is the recommended method of caching.
|
* the moderator interface.
|
||||||
*
|
*
|
||||||
* https://github.com/vichan-devel/vichan/wiki/cache
|
* https://github.com/vichan-devel/vichan/wiki/cache
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Uses a PHP array. MUST NOT be used in multiprocess environments.
|
||||||
$config['cache']['enabled'] = 'php';
|
$config['cache']['enabled'] = 'php';
|
||||||
|
// The recommended in-memory method of caching. Requires the extension. Due to how APCu works, this should be
|
||||||
|
// disabled when you run tools from the cli.
|
||||||
// $config['cache']['enabled'] = 'apcu';
|
// $config['cache']['enabled'] = 'apcu';
|
||||||
|
// The Memcache server. Requires the memcached extension, with a final D.
|
||||||
// $config['cache']['enabled'] = 'memcached';
|
// $config['cache']['enabled'] = 'memcached';
|
||||||
|
// The Redis server. Requires the extension.
|
||||||
// $config['cache']['enabled'] = 'redis';
|
// $config['cache']['enabled'] = 'redis';
|
||||||
|
// Use the local cache folder. Slower than native but available out of the box and compatible with multiprocess
|
||||||
|
// environments. You can mount a ram-based filesystem in the cache directory to improve performance.
|
||||||
// $config['cache']['enabled'] = 'fs';
|
// $config['cache']['enabled'] = 'fs';
|
||||||
|
// Technically available, offers a no-op fake cache. Don't use this outside of testing or debugging.
|
||||||
|
// $config['cache']['enabled'] = 'none';
|
||||||
|
|
||||||
// Timeout for cached objects such as posts and HTML.
|
// Timeout for cached objects such as posts and HTML.
|
||||||
$config['cache']['timeout'] = 60 * 60 * 48; // 48 hours
|
$config['cache']['timeout'] = 60 * 60 * 48; // 48 hours
|
||||||
|
@ -83,6 +83,10 @@ function build_context(array $config): Context {
|
|||||||
$config['captcha']['native']['provider_check'],
|
$config['captcha']['native']['provider_check'],
|
||||||
$config['captcha']['native']['extra']
|
$config['captcha']['native']['extra']
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
CacheDriver::class => function($c) {
|
||||||
|
// Use the global for backwards compatibility.
|
||||||
|
return \cache::getCache();
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
use Vichan\Context;
|
use Vichan\Context;
|
||||||
use Vichan\Functions\Format;
|
use Vichan\Functions\Format;
|
||||||
|
|
||||||
use Vichan\Functions\Net;
|
use Vichan\Functions\Net;
|
||||||
|
use Vichan\Data\Driver\CacheDriver;
|
||||||
|
|
||||||
defined('TINYBOARD') or exit;
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
@ -112,25 +112,23 @@ function mod_dashboard(Context $ctx) {
|
|||||||
$args['boards'] = listBoards();
|
$args['boards'] = listBoards();
|
||||||
|
|
||||||
if (hasPermission($config['mod']['noticeboard'])) {
|
if (hasPermission($config['mod']['noticeboard'])) {
|
||||||
if (!$config['cache']['enabled'] || !$args['noticeboard'] = cache::get('noticeboard_preview')) {
|
if (!$args['noticeboard'] = $ctx->get(CacheDriver::class)->get('noticeboard_preview')) {
|
||||||
$query = prepare("SELECT ``noticeboard``.*, `username` FROM ``noticeboard`` LEFT JOIN ``mods`` ON ``mods``.`id` = `mod` ORDER BY `id` DESC LIMIT :limit");
|
$query = prepare("SELECT ``noticeboard``.*, `username` FROM ``noticeboard`` LEFT JOIN ``mods`` ON ``mods``.`id` = `mod` ORDER BY `id` DESC LIMIT :limit");
|
||||||
$query->bindValue(':limit', $config['mod']['noticeboard_dashboard'], PDO::PARAM_INT);
|
$query->bindValue(':limit', $config['mod']['noticeboard_dashboard'], PDO::PARAM_INT);
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
$args['noticeboard'] = $query->fetchAll(PDO::FETCH_ASSOC);
|
$args['noticeboard'] = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if ($config['cache']['enabled'])
|
$ctx->get(CacheDriver::class)->set('noticeboard_preview', $args['noticeboard']);
|
||||||
cache::set('noticeboard_preview', $args['noticeboard']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$config['cache']['enabled'] || ($args['unread_pms'] = cache::get('pm_unreadcount_' . $mod['id'])) === false) {
|
if ($args['unread_pms'] = $ctx->get(CacheDriver::class)->get('pm_unreadcount_' . $mod['id']) === false) {
|
||||||
$query = prepare('SELECT COUNT(*) FROM ``pms`` WHERE `to` = :id AND `unread` = 1');
|
$query = prepare('SELECT COUNT(*) FROM ``pms`` WHERE `to` = :id AND `unread` = 1');
|
||||||
$query->bindValue(':id', $mod['id']);
|
$query->bindValue(':id', $mod['id']);
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
$args['unread_pms'] = $query->fetchColumn();
|
$args['unread_pms'] = $query->fetchColumn();
|
||||||
|
|
||||||
if ($config['cache']['enabled'])
|
$ctx->get(CacheDriver::class)->set('pm_unreadcount_' . $mod['id'], $args['unread_pms']);
|
||||||
cache::set('pm_unreadcount_' . $mod['id'], $args['unread_pms']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = query('SELECT COUNT(*) FROM ``reports``') or error(db_error($query));
|
$query = query('SELECT COUNT(*) FROM ``reports``') or error(db_error($query));
|
||||||
@ -384,6 +382,8 @@ function mod_search(Context $ctx, $type, $search_query_escaped, $page_no = 1) {
|
|||||||
function mod_edit_board(Context $ctx, $boardName) {
|
function mod_edit_board(Context $ctx, $boardName) {
|
||||||
global $board, $config, $mod;
|
global $board, $config, $mod;
|
||||||
|
|
||||||
|
$cache = $ctx->get(CacheDriver::class);
|
||||||
|
|
||||||
if (!openBoard($boardName))
|
if (!openBoard($boardName))
|
||||||
error($config['error']['noboard']);
|
error($config['error']['noboard']);
|
||||||
|
|
||||||
@ -399,10 +399,8 @@ function mod_edit_board(Context $ctx, $boardName) {
|
|||||||
$query->bindValue(':uri', $board['uri']);
|
$query->bindValue(':uri', $board['uri']);
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
if ($config['cache']['enabled']) {
|
$cache->delete('board_' . $board['uri']);
|
||||||
cache::delete('board_' . $board['uri']);
|
$cache->delete('all_boards');
|
||||||
cache::delete('all_boards');
|
|
||||||
}
|
|
||||||
|
|
||||||
modLog('Deleted board: ' . sprintf($config['board_abbreviation'], $board['uri']), false);
|
modLog('Deleted board: ' . sprintf($config['board_abbreviation'], $board['uri']), false);
|
||||||
|
|
||||||
@ -467,10 +465,9 @@ function mod_edit_board(Context $ctx, $boardName) {
|
|||||||
modLog('Edited board information for ' . sprintf($config['board_abbreviation'], $board['uri']), false);
|
modLog('Edited board information for ' . sprintf($config['board_abbreviation'], $board['uri']), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($config['cache']['enabled']) {
|
$cache->delete('board_' . $board['uri']);
|
||||||
cache::delete('board_' . $board['uri']);
|
$cache->delete('all_boards');
|
||||||
cache::delete('all_boards');
|
|
||||||
}
|
|
||||||
|
|
||||||
Vichan\Functions\Theme\rebuild_themes('boards');
|
Vichan\Functions\Theme\rebuild_themes('boards');
|
||||||
|
|
||||||
@ -505,6 +502,8 @@ function mod_new_board(Context $ctx) {
|
|||||||
if (!preg_match('/^' . $config['board_regex'] . '$/u', $_POST['uri']))
|
if (!preg_match('/^' . $config['board_regex'] . '$/u', $_POST['uri']))
|
||||||
error(sprintf($config['error']['invalidfield'], 'URI'));
|
error(sprintf($config['error']['invalidfield'], 'URI'));
|
||||||
|
|
||||||
|
$cache = $ctx->get(CacheDriver::class);
|
||||||
|
|
||||||
$bytes = 0;
|
$bytes = 0;
|
||||||
$chars = preg_split('//u', $_POST['uri'], -1, PREG_SPLIT_NO_EMPTY);
|
$chars = preg_split('//u', $_POST['uri'], -1, PREG_SPLIT_NO_EMPTY);
|
||||||
foreach ($chars as $char) {
|
foreach ($chars as $char) {
|
||||||
@ -544,8 +543,8 @@ function mod_new_board(Context $ctx) {
|
|||||||
|
|
||||||
query($query) or error(db_error());
|
query($query) or error(db_error());
|
||||||
|
|
||||||
if ($config['cache']['enabled'])
|
$cache = $ctx->get(CacheDriver::class);
|
||||||
cache::delete('all_boards');
|
$cache->delete('all_boards');
|
||||||
|
|
||||||
// Build the board
|
// Build the board
|
||||||
buildIndex();
|
buildIndex();
|
||||||
@ -590,8 +589,8 @@ function mod_noticeboard(Context $ctx, $page_no = 1) {
|
|||||||
$query->bindValue(':body', $_POST['body']);
|
$query->bindValue(':body', $_POST['body']);
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
if ($config['cache']['enabled'])
|
$cache = $ctx->get(CacheDriver::class);
|
||||||
cache::delete('noticeboard_preview');
|
$cache->delete('noticeboard_preview');
|
||||||
|
|
||||||
modLog('Posted a noticeboard entry');
|
modLog('Posted a noticeboard entry');
|
||||||
|
|
||||||
@ -631,7 +630,7 @@ function mod_noticeboard_delete(Context $ctx, $id) {
|
|||||||
$config = $ctx->get('config');
|
$config = $ctx->get('config');
|
||||||
|
|
||||||
if (!hasPermission($config['mod']['noticeboard_delete']))
|
if (!hasPermission($config['mod']['noticeboard_delete']))
|
||||||
error($config['error']['noaccess']);
|
error($config['error']['noaccess']);
|
||||||
|
|
||||||
$query = prepare('DELETE FROM ``noticeboard`` WHERE `id` = :id');
|
$query = prepare('DELETE FROM ``noticeboard`` WHERE `id` = :id');
|
||||||
$query->bindValue(':id', $id);
|
$query->bindValue(':id', $id);
|
||||||
@ -639,8 +638,8 @@ function mod_noticeboard_delete(Context $ctx, $id) {
|
|||||||
|
|
||||||
modLog('Deleted a noticeboard entry');
|
modLog('Deleted a noticeboard entry');
|
||||||
|
|
||||||
if ($config['cache']['enabled'])
|
$cache = $ctx->get(CacheDriver::class);
|
||||||
cache::delete('noticeboard_preview');
|
$cache->delete('noticeboard_preview');
|
||||||
|
|
||||||
header('Location: ?/noticeboard', true, $config['redirect_http']);
|
header('Location: ?/noticeboard', true, $config['redirect_http']);
|
||||||
}
|
}
|
||||||
@ -706,7 +705,7 @@ function mod_news_delete(Context $ctx, $id) {
|
|||||||
$config = $ctx->get('config');
|
$config = $ctx->get('config');
|
||||||
|
|
||||||
if (!hasPermission($config['mod']['news_delete']))
|
if (!hasPermission($config['mod']['news_delete']))
|
||||||
error($config['error']['noaccess']);
|
error($config['error']['noaccess']);
|
||||||
|
|
||||||
$query = prepare('DELETE FROM ``news`` WHERE `id` = :id');
|
$query = prepare('DELETE FROM ``news`` WHERE `id` = :id');
|
||||||
$query->bindValue(':id', $id);
|
$query->bindValue(':id', $id);
|
||||||
@ -843,7 +842,7 @@ function mod_view_board(Context $ctx, $boardName, $page_no = 1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$page['pages'] = getPages(true);
|
$page['pages'] = getPages(true);
|
||||||
$page['pages'][$page_no-1]['selected'] = true;
|
$page['pages'][$page_no - 1]['selected'] = true;
|
||||||
$page['btn'] = getPageButtons($page['pages'], true);
|
$page['btn'] = getPageButtons($page['pages'], true);
|
||||||
$page['mod'] = true;
|
$page['mod'] = true;
|
||||||
$page['config'] = $config;
|
$page['config'] = $config;
|
||||||
@ -1042,7 +1041,6 @@ function mod_edit_ban(Context $ctx, $ban_id) {
|
|||||||
Bans::delete($ban_id);
|
Bans::delete($ban_id);
|
||||||
|
|
||||||
header('Location: ?/', true, $config['redirect_http']);
|
header('Location: ?/', true, $config['redirect_http']);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$args['token'] = make_secure_link_token('edit_ban/' . $ban_id);
|
$args['token'] = make_secure_link_token('edit_ban/' . $ban_id);
|
||||||
@ -2235,10 +2233,9 @@ function mod_pm(Context $ctx, $id, $reply = false) {
|
|||||||
$query->bindValue(':id', $id);
|
$query->bindValue(':id', $id);
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
if ($config['cache']['enabled']) {
|
$cache = $ctx->get(CacheDriver::class);
|
||||||
cache::delete('pm_unread_' . $mod['id']);
|
$cache->delete('pm_unread_' . $mod['id']);
|
||||||
cache::delete('pm_unreadcount_' . $mod['id']);
|
$cache->delete('pm_unreadcount_' . $mod['id']);
|
||||||
}
|
|
||||||
|
|
||||||
header('Location: ?/', true, $config['redirect_http']);
|
header('Location: ?/', true, $config['redirect_http']);
|
||||||
return;
|
return;
|
||||||
@ -2249,10 +2246,9 @@ function mod_pm(Context $ctx, $id, $reply = false) {
|
|||||||
$query->bindValue(':id', $id);
|
$query->bindValue(':id', $id);
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
if ($config['cache']['enabled']) {
|
$cache = $ctx->get(CacheDriver::class);
|
||||||
cache::delete('pm_unread_' . $mod['id']);
|
$cache->delete('pm_unread_' . $mod['id']);
|
||||||
cache::delete('pm_unreadcount_' . $mod['id']);
|
$cache->delete('pm_unreadcount_' . $mod['id']);
|
||||||
}
|
|
||||||
|
|
||||||
modLog('Read a PM');
|
modLog('Read a PM');
|
||||||
}
|
}
|
||||||
@ -2339,10 +2335,10 @@ function mod_new_pm(Context $ctx, $username) {
|
|||||||
$query->bindValue(':time', time());
|
$query->bindValue(':time', time());
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
if ($config['cache']['enabled']) {
|
$cache = $ctx->get(CacheDriver::class);
|
||||||
cache::delete('pm_unread_' . $id);
|
|
||||||
cache::delete('pm_unreadcount_' . $id);
|
$cache->delete('pm_unread_' . $id);
|
||||||
}
|
$cache->delete('pm_unreadcount_' . $id);
|
||||||
|
|
||||||
modLog('Sent a PM to ' . utf8tohtml($username));
|
modLog('Sent a PM to ' . utf8tohtml($username));
|
||||||
|
|
||||||
@ -2368,6 +2364,8 @@ function mod_rebuild(Context $ctx) {
|
|||||||
if (!hasPermission($config['mod']['rebuild']))
|
if (!hasPermission($config['mod']['rebuild']))
|
||||||
error($config['error']['noaccess']);
|
error($config['error']['noaccess']);
|
||||||
|
|
||||||
|
$cache = $ctx->get(CacheDriver::class);
|
||||||
|
|
||||||
if (isset($_POST['rebuild'])) {
|
if (isset($_POST['rebuild'])) {
|
||||||
@set_time_limit($config['mod']['rebuild_timelimit']);
|
@set_time_limit($config['mod']['rebuild_timelimit']);
|
||||||
|
|
||||||
@ -2378,7 +2376,7 @@ function mod_rebuild(Context $ctx) {
|
|||||||
if (isset($_POST['rebuild_cache'])) {
|
if (isset($_POST['rebuild_cache'])) {
|
||||||
if ($config['cache']['enabled']) {
|
if ($config['cache']['enabled']) {
|
||||||
$log[] = 'Flushing cache';
|
$log[] = 'Flushing cache';
|
||||||
Cache::flush();
|
$cache->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
$log[] = 'Clearing template cache';
|
$log[] = 'Clearing template cache';
|
||||||
@ -2840,6 +2838,8 @@ function mod_theme_configure(Context $ctx, $theme_name) {
|
|||||||
error($config['error']['invalidtheme']);
|
error($config['error']['invalidtheme']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$cache = $ctx->get(CacheDriver::class);
|
||||||
|
|
||||||
if (isset($_POST['install'])) {
|
if (isset($_POST['install'])) {
|
||||||
// Check if everything is submitted
|
// Check if everything is submitted
|
||||||
foreach ($theme['config'] as &$conf) {
|
foreach ($theme['config'] as &$conf) {
|
||||||
@ -2868,8 +2868,8 @@ function mod_theme_configure(Context $ctx, $theme_name) {
|
|||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
// Clean cache
|
// Clean cache
|
||||||
Cache::delete("themes");
|
$cache->delete("themes");
|
||||||
Cache::delete("theme_settings_".$theme_name);
|
$cache->delete("theme_settings_$theme_name");
|
||||||
|
|
||||||
$result = true;
|
$result = true;
|
||||||
$message = false;
|
$message = false;
|
||||||
@ -2928,13 +2928,15 @@ function mod_theme_uninstall(Context $ctx, $theme_name) {
|
|||||||
if (!hasPermission($config['mod']['themes']))
|
if (!hasPermission($config['mod']['themes']))
|
||||||
error($config['error']['noaccess']);
|
error($config['error']['noaccess']);
|
||||||
|
|
||||||
|
$cache = $ctx->get(CacheDriver::class);
|
||||||
|
|
||||||
$query = prepare("DELETE FROM ``theme_settings`` WHERE `theme` = :theme");
|
$query = prepare("DELETE FROM ``theme_settings`` WHERE `theme` = :theme");
|
||||||
$query->bindValue(':theme', $theme_name);
|
$query->bindValue(':theme', $theme_name);
|
||||||
$query->execute() or error(db_error($query));
|
$query->execute() or error(db_error($query));
|
||||||
|
|
||||||
// Clean cache
|
// Clean cache
|
||||||
Cache::delete("themes");
|
$cache->delete("themes");
|
||||||
Cache::delete("theme_settings_".$theme_name);
|
$cache->delete("theme_settings_$theme_name");
|
||||||
|
|
||||||
header('Location: ?/themes', true, $config['redirect_http']);
|
header('Location: ?/themes', true, $config['redirect_http']);
|
||||||
}
|
}
|
||||||
@ -2959,7 +2961,7 @@ function mod_theme_rebuild(Context $ctx, $theme_name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This needs to be done for `secure` CSRF prevention compatibility, otherwise the $board will be read in as the token if editing global pages.
|
// This needs to be done for `secure` CSRF prevention compatibility, otherwise the $board will be read in as the token if editing global pages.
|
||||||
function delete_page_base($page = '', $board = false) {
|
function delete_page_base(Context $ctx, $page = '', $board = false) {
|
||||||
global $config, $mod;
|
global $config, $mod;
|
||||||
|
|
||||||
if (empty($board))
|
if (empty($board))
|
||||||
|
@ -21,5 +21,20 @@ echo "Deleted $deleted_count expired antispam in $delta seconds!\n";
|
|||||||
$time_tot = $delta;
|
$time_tot = $delta;
|
||||||
$deleted_tot = $deleted_count;
|
$deleted_tot = $deleted_count;
|
||||||
|
|
||||||
|
if ($config['cache']['enabled'] === 'fs') {
|
||||||
|
$fs_cache = new Vichan\Data\Driver\FsCacheDriver(
|
||||||
|
$config['cache']['prefix'],
|
||||||
|
"tmp/cache/{$config['cache']['prefix']}",
|
||||||
|
'.lock',
|
||||||
|
false
|
||||||
|
);
|
||||||
|
$start = microtime(true);
|
||||||
|
$fs_cache->collect();
|
||||||
|
$delta = microtime(true) - $start;
|
||||||
|
echo "Deleted $deleted_count expired filesystem cache items in $delta seconds!\n";
|
||||||
|
$time_tot = $delta;
|
||||||
|
$deleted_tot = $deleted_count;
|
||||||
|
}
|
||||||
|
|
||||||
$time_tot = number_format((float)$time_tot, 4, '.', '');
|
$time_tot = number_format((float)$time_tot, 4, '.', '');
|
||||||
modLog("Deleted $deleted_tot expired entries in {$time_tot}s with maintenance tool");
|
modLog("Deleted $deleted_tot expired entries in {$time_tot}s with maintenance tool");
|
||||||
|
Loading…
Reference in New Issue
Block a user