Have shimmie's global cache object implement PSR-16
This commit is contained in:
parent
e79470d974
commit
708e102338
20 changed files with 136 additions and 44 deletions
|
@ -49,7 +49,8 @@
|
||||||
|
|
||||||
"bower-asset/jquery" : "^1.12",
|
"bower-asset/jquery" : "^1.12",
|
||||||
"bower-asset/jquery-timeago" : "^1.5",
|
"bower-asset/jquery-timeago" : "^1.5",
|
||||||
"bower-asset/js-cookie" : "^2.1"
|
"bower-asset/js-cookie" : "^2.1",
|
||||||
|
"psr/simple-cache": "3.0.x-dev"
|
||||||
},
|
},
|
||||||
|
|
||||||
"require-dev" : {
|
"require-dev" : {
|
||||||
|
|
55
composer.lock
generated
55
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "1a9bc743870e5e5749b80ee7cdb07086",
|
"content-hash": "c5b40df44e9d52a91768469533768052",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bower-asset/jquery",
|
"name": "bower-asset/jquery",
|
||||||
|
@ -282,6 +282,58 @@
|
||||||
},
|
},
|
||||||
"type": "library"
|
"type": "library"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/simple-cache",
|
||||||
|
"version": "dev-master",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/simple-cache.git",
|
||||||
|
"reference": "2d280c2aaa23a120f35d55cfde8581954a8e77fa"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/simple-cache/zipball/2d280c2aaa23a120f35d55cfde8581954a8e77fa",
|
||||||
|
"reference": "2d280c2aaa23a120f35d55cfde8581954a8e77fa",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"default-branch": true,
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\SimpleCache\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interfaces for simple caching",
|
||||||
|
"keywords": [
|
||||||
|
"cache",
|
||||||
|
"caching",
|
||||||
|
"psr",
|
||||||
|
"psr-16",
|
||||||
|
"simple-cache"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/simple-cache/tree/master"
|
||||||
|
},
|
||||||
|
"time": "2022-04-08T16:41:45+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "shish/eventtracer-php",
|
"name": "shish/eventtracer-php",
|
||||||
"version": "v2.0.1",
|
"version": "v2.0.1",
|
||||||
|
@ -5254,6 +5306,7 @@
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"stability-flags": {
|
"stability-flags": {
|
||||||
"shish/gqla": 20,
|
"shish/gqla": 20,
|
||||||
|
"psr/simple-cache": 20,
|
||||||
"scrutinizer/ocular": 20
|
"scrutinizer/ocular": 20
|
||||||
},
|
},
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
|
|
|
@ -135,32 +135,19 @@ class RedisCache implements CacheEngine
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Cache
|
class CacheWithStats implements \Psr\SimpleCache\CacheInterface
|
||||||
{
|
{
|
||||||
public $engine;
|
public $engine;
|
||||||
public int $hits=0;
|
public int $hits=0;
|
||||||
public int $misses=0;
|
public int $misses=0;
|
||||||
public int $time=0;
|
public int $time=0;
|
||||||
|
|
||||||
public function __construct(?string $dsn)
|
public function __construct(CacheEngine $c)
|
||||||
{
|
{
|
||||||
$matches = [];
|
|
||||||
$c = null;
|
|
||||||
if ($dsn && preg_match("#(.*)://(.*)#", $dsn, $matches) && !isset($_GET['DISABLE_CACHE'])) {
|
|
||||||
if ($matches[1] == "memcached" || $matches[1] == "memcache") {
|
|
||||||
$c = new MemcachedCache($matches[2]);
|
|
||||||
} elseif ($matches[1] == "apc") {
|
|
||||||
$c = new APCCache($matches[2]);
|
|
||||||
} elseif ($matches[1] == "redis") {
|
|
||||||
$c = new RedisCache($matches[2]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$c = new NoCache();
|
|
||||||
}
|
|
||||||
$this->engine = $c;
|
$this->engine = $c;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get(string $key)
|
public function get(string $key, mixed $default=null): mixed
|
||||||
{
|
{
|
||||||
global $_tracer;
|
global $_tracer;
|
||||||
$_tracer->begin("Cache Query", ["key"=>$key]);
|
$_tracer->begin("Cache Query", ["key"=>$key]);
|
||||||
|
@ -170,26 +157,60 @@ class Cache
|
||||||
$this->hits++;
|
$this->hits++;
|
||||||
} else {
|
} else {
|
||||||
$res = "miss";
|
$res = "miss";
|
||||||
|
$val = $default;
|
||||||
$this->misses++;
|
$this->misses++;
|
||||||
}
|
}
|
||||||
$_tracer->end(null, ["result"=>$res]);
|
$_tracer->end(null, ["result"=>$res]);
|
||||||
return $val;
|
return $val;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function set(string $key, $val, int $time=0)
|
public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool
|
||||||
{
|
{
|
||||||
global $_tracer;
|
global $_tracer;
|
||||||
$_tracer->begin("Cache Set", ["key"=>$key, "time"=>$time]);
|
$_tracer->begin("Cache Set", ["key"=>$key, "ttl"=>$ttl]);
|
||||||
$this->engine->set($key, $val, $time);
|
$this->engine->set($key, $value, $ttl ?? 0);
|
||||||
$_tracer->end();
|
$_tracer->end();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(string $key)
|
public function delete(string $key): bool
|
||||||
{
|
{
|
||||||
global $_tracer;
|
global $_tracer;
|
||||||
$_tracer->begin("Cache Delete", ["key"=>$key]);
|
$_tracer->begin("Cache Delete", ["key"=>$key]);
|
||||||
$this->engine->delete($key);
|
$this->engine->delete($key);
|
||||||
$_tracer->end();
|
$_tracer->end();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clear(): bool
|
||||||
|
{
|
||||||
|
throw new Exception("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMultiple(iterable $keys, mixed $default = null): iterable
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
foreach($keys as $key) {
|
||||||
|
$results[$key] = $this->get($key, $default);
|
||||||
|
}
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool {
|
||||||
|
foreach($values as $key => $value) {
|
||||||
|
$this->set($key, $value, $ttl);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteMultiple(iterable $keys): bool {
|
||||||
|
foreach($keys as $key) {
|
||||||
|
$this->delete($key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public function has(string $key): bool {
|
||||||
|
$sentinel = 4345345735673;
|
||||||
|
return $this->get($key, $sentinel) != $sentinel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_hits(): int
|
public function get_hits(): int
|
||||||
|
@ -201,3 +222,20 @@ class Cache
|
||||||
return $this->misses;
|
return $this->misses;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadCache(?string $dsn): CacheWithStats {
|
||||||
|
$matches = [];
|
||||||
|
$c = null;
|
||||||
|
if ($dsn && preg_match("#(.*)://(.*)#", $dsn, $matches) && !isset($_GET['DISABLE_CACHE'])) {
|
||||||
|
if ($matches[1] == "memcached" || $matches[1] == "memcache") {
|
||||||
|
$c = new MemcachedCache($matches[2]);
|
||||||
|
} elseif ($matches[1] == "apc") {
|
||||||
|
$c = new APCCache($matches[2]);
|
||||||
|
} elseif ($matches[1] == "redis") {
|
||||||
|
$c = new RedisCache($matches[2]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$c = new NoCache();
|
||||||
|
}
|
||||||
|
return new CacheWithStats($c);
|
||||||
|
}
|
||||||
|
|
|
@ -284,7 +284,7 @@ class DatabaseConfig extends BaseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
$cached = $cache->get($cache_name);
|
$cached = $cache->get($cache_name);
|
||||||
if ($cached) {
|
if (!is_null($cached)) {
|
||||||
$this->values = $cached;
|
$this->values = $cached;
|
||||||
} else {
|
} else {
|
||||||
$this->values = [];
|
$this->values = [];
|
||||||
|
|
|
@ -185,7 +185,7 @@ class Image
|
||||||
{
|
{
|
||||||
global $cache, $database;
|
global $cache, $database;
|
||||||
$total = $cache->get("image-count");
|
$total = $cache->get("image-count");
|
||||||
if (!$total) {
|
if (is_null($total)) {
|
||||||
$total = (int)$database->get_one("SELECT COUNT(*) FROM images");
|
$total = (int)$database->get_one("SELECT COUNT(*) FROM images");
|
||||||
$cache->set("image-count", $total, 600);
|
$cache->set("image-count", $total, 600);
|
||||||
}
|
}
|
||||||
|
@ -227,7 +227,7 @@ class Image
|
||||||
// implode(tags) can be too long for memcache...
|
// implode(tags) can be too long for memcache...
|
||||||
$cache_key = "image-count:" . md5(Tag::implode($tags));
|
$cache_key = "image-count:" . md5(Tag::implode($tags));
|
||||||
$total = $cache->get($cache_key);
|
$total = $cache->get($cache_key);
|
||||||
if (!$total) {
|
if (is_null($total)) {
|
||||||
if (Extension::is_enabled(RatingsInfo::KEY)) {
|
if (Extension::is_enabled(RatingsInfo::KEY)) {
|
||||||
$tags[] = "rating:*";
|
$tags[] = "rating:*";
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ class TagUsage {
|
||||||
}
|
}
|
||||||
|
|
||||||
$res = $cache->get($cache_key);
|
$res = $cache->get($cache_key);
|
||||||
if (!$res) {
|
if (is_null($res)) {
|
||||||
$res = $database->get_pairs(
|
$res = $database->get_pairs(
|
||||||
"
|
"
|
||||||
SELECT tag, count
|
SELECT tag, count
|
||||||
|
|
|
@ -74,7 +74,7 @@ class User
|
||||||
{
|
{
|
||||||
global $cache, $config, $database;
|
global $cache, $config, $database;
|
||||||
$row = $cache->get("user-session:$name-$session");
|
$row = $cache->get("user-session:$name-$session");
|
||||||
if (!$row) {
|
if (is_null($row)) {
|
||||||
if ($database->get_driver_id() === DatabaseDriverID::MYSQL) {
|
if ($database->get_driver_id() === DatabaseDriverID::MYSQL) {
|
||||||
$query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess";
|
$query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess";
|
||||||
} else {
|
} else {
|
||||||
|
@ -91,7 +91,7 @@ class User
|
||||||
global $cache, $database;
|
global $cache, $database;
|
||||||
if ($id === 1) {
|
if ($id === 1) {
|
||||||
$cached = $cache->get('user-id:'.$id);
|
$cached = $cache->get('user-id:'.$id);
|
||||||
if ($cached) {
|
if (!is_null($cached)) {
|
||||||
return new User($cached);
|
return new User($cached);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ class AutoComplete extends Extension
|
||||||
}
|
}
|
||||||
|
|
||||||
$res = $cache->get($cache_key);
|
$res = $cache->get($cache_key);
|
||||||
if (!$res) {
|
if (is_null($res)) {
|
||||||
$res = $database->get_pairs(
|
$res = $database->get_pairs(
|
||||||
"
|
"
|
||||||
SELECT tag, count
|
SELECT tag, count
|
||||||
|
|
|
@ -49,7 +49,7 @@ class Blocks extends Extension
|
||||||
global $cache, $database, $page, $user;
|
global $cache, $database, $page, $user;
|
||||||
|
|
||||||
$blocks = $cache->get("blocks");
|
$blocks = $cache->get("blocks");
|
||||||
if ($blocks === false) {
|
if (is_null($blocks)) {
|
||||||
$blocks = $database->get_all("SELECT * FROM blocks");
|
$blocks = $database->get_all("SELECT * FROM blocks");
|
||||||
$cache->set("blocks", $blocks, 600);
|
$cache->set("blocks", $blocks, 600);
|
||||||
}
|
}
|
||||||
|
|
|
@ -274,7 +274,7 @@ class CommentList extends Extension
|
||||||
$where = SPEED_HAX ? "WHERE posted > now() - interval '24 hours'" : "";
|
$where = SPEED_HAX ? "WHERE posted > now() - interval '24 hours'" : "";
|
||||||
|
|
||||||
$total_pages = $cache->get("comment_pages");
|
$total_pages = $cache->get("comment_pages");
|
||||||
if (empty($total_pages)) {
|
if (is_null($total_pages)) {
|
||||||
$total_pages = (int)ceil($database->get_one("
|
$total_pages = (int)ceil($database->get_one("
|
||||||
SELECT COUNT(c1)
|
SELECT COUNT(c1)
|
||||||
FROM (SELECT COUNT(image_id) AS c1 FROM comments $where GROUP BY image_id) AS s1
|
FROM (SELECT COUNT(image_id) AS c1 FROM comments $where GROUP BY image_id) AS s1
|
||||||
|
@ -346,7 +346,7 @@ class CommentList extends Extension
|
||||||
$cc = $config->get_int("comment_count");
|
$cc = $config->get_int("comment_count");
|
||||||
if ($cc > 0) {
|
if ($cc > 0) {
|
||||||
$recent = $cache->get("recent_comments");
|
$recent = $cache->get("recent_comments");
|
||||||
if (empty($recent)) {
|
if (is_null($recent)) {
|
||||||
$recent = $this->get_recent_comments($cc);
|
$recent = $this->get_recent_comments($cc);
|
||||||
$cache->set("recent_comments", $recent, 60);
|
$cache->set("recent_comments", $recent, 60);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ class Featured extends Extension
|
||||||
$fid = $config->get_int("featured_id");
|
$fid = $config->get_int("featured_id");
|
||||||
if ($fid > 0) {
|
if ($fid > 0) {
|
||||||
$image = $cache->get("featured_image_object:$fid");
|
$image = $cache->get("featured_image_object:$fid");
|
||||||
if ($image === false) {
|
if (is_null($image)) {
|
||||||
$image = Image::by_id($fid);
|
$image = Image::by_id($fid);
|
||||||
if ($image) { // make sure the object is fully populated before saving
|
if ($image) { // make sure the object is fully populated before saving
|
||||||
$image->get_tag_array();
|
$image->get_tag_array();
|
||||||
|
|
|
@ -85,7 +85,7 @@ class Index extends Extension
|
||||||
if ($count_search_terms === 0 && ($page_number < 10)) {
|
if ($count_search_terms === 0 && ($page_number < 10)) {
|
||||||
// extra caching for the first few post/list pages
|
// extra caching for the first few post/list pages
|
||||||
$images = $cache->get("post-list:$page_number");
|
$images = $cache->get("post-list:$page_number");
|
||||||
if (!$images) {
|
if (is_null($images)) {
|
||||||
$images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms);
|
$images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms);
|
||||||
$cache->set("post-list:$page_number", $images, 60);
|
$cache->set("post-list:$page_number", $images, 60);
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ class IPBan extends Extension
|
||||||
// Get lists of banned IPs and banned networks
|
// Get lists of banned IPs and banned networks
|
||||||
$ips = $cache->get("ip_bans");
|
$ips = $cache->get("ip_bans");
|
||||||
$networks = $cache->get("network_bans");
|
$networks = $cache->get("network_bans");
|
||||||
if ($ips === false || $networks === false) {
|
if (is_null($ips) || is_null($networks)) {
|
||||||
$rows = $database->get_pairs("
|
$rows = $database->get_pairs("
|
||||||
SELECT ip, id
|
SELECT ip, id
|
||||||
FROM bans
|
FROM bans
|
||||||
|
|
|
@ -241,7 +241,7 @@ class PrivMsg extends Extension
|
||||||
global $cache, $database;
|
global $cache, $database;
|
||||||
|
|
||||||
$count = $cache->get("pm-count:{$user->id}");
|
$count = $cache->get("pm-count:{$user->id}");
|
||||||
if (is_null($count) || $count === false) {
|
if (is_null($count)) {
|
||||||
$count = $database->get_one("
|
$count = $database->get_one("
|
||||||
SELECT count(*)
|
SELECT count(*)
|
||||||
FROM private_message
|
FROM private_message
|
||||||
|
|
|
@ -243,7 +243,7 @@ class ReportImage extends Extension
|
||||||
global $cache, $database;
|
global $cache, $database;
|
||||||
|
|
||||||
$count = $cache->get("image-report-count");
|
$count = $cache->get("image-report-count");
|
||||||
if (is_null($count) || $count === false) {
|
if (is_null($count)) {
|
||||||
$count = $database->get_one("SELECT count(*) FROM image_reports");
|
$count = $database->get_one("SELECT count(*) FROM image_reports");
|
||||||
$cache->set("image-report-count", $count, 600);
|
$cache->set("image-report-count", $count, 600);
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ class RSSImages extends Extension
|
||||||
global $cache;
|
global $cache;
|
||||||
|
|
||||||
$cached = $cache->get("rss-item-image:{$image->id}");
|
$cached = $cache->get("rss-item-image:{$image->id}");
|
||||||
if ($cached) {
|
if (!is_null($cached)) {
|
||||||
return $cached;
|
return $cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -157,7 +157,7 @@ class TagList extends Extension
|
||||||
|
|
||||||
$results = $cache->get("tag_list_omitted_tags:".$tags_config);
|
$results = $cache->get("tag_list_omitted_tags:".$tags_config);
|
||||||
|
|
||||||
if ($results==null) {
|
if (is_null($results)) {
|
||||||
$tags = explode(" ", $tags_config);
|
$tags = explode(" ", $tags_config);
|
||||||
|
|
||||||
if (empty($tags)) {
|
if (empty($tags)) {
|
||||||
|
@ -494,7 +494,7 @@ class TagList extends Extension
|
||||||
global $cache, $database, $config;
|
global $cache, $database, $config;
|
||||||
|
|
||||||
$tags = $cache->get("popular_tags");
|
$tags = $cache->get("popular_tags");
|
||||||
if (empty($tags)) {
|
if (is_null($tags)) {
|
||||||
$omitted_tags = self::get_omitted_tags();
|
$omitted_tags = self::get_omitted_tags();
|
||||||
|
|
||||||
if (empty($omitted_tags)) {
|
if (empty($omitted_tags)) {
|
||||||
|
@ -556,7 +556,7 @@ class TagList extends Extension
|
||||||
$str_search = Tag::implode($search);
|
$str_search = Tag::implode($search);
|
||||||
$related_tags = $cache->get("related_tags:$str_search");
|
$related_tags = $cache->get("related_tags:$str_search");
|
||||||
|
|
||||||
if (empty($related_tags)) {
|
if (is_null($related_tags)) {
|
||||||
// $search_tags = array();
|
// $search_tags = array();
|
||||||
|
|
||||||
$starting_tags = [];
|
$starting_tags = [];
|
||||||
|
|
|
@ -46,7 +46,7 @@ _set_up_shimmie_environment();
|
||||||
$_tracer = new \EventTracer();
|
$_tracer = new \EventTracer();
|
||||||
$_tracer->begin("Bootstrap");
|
$_tracer->begin("Bootstrap");
|
||||||
_load_core_files();
|
_load_core_files();
|
||||||
$cache = new Cache(CACHE_DSN);
|
$cache = loadCache(CACHE_DSN);
|
||||||
$database = new Database(DATABASE_DSN);
|
$database = new Database(DATABASE_DSN);
|
||||||
$config = new DatabaseConfig($database);
|
$config = new DatabaseConfig($database);
|
||||||
ExtensionInfo::load_all_extension_info();
|
ExtensionInfo::load_all_extension_info();
|
||||||
|
|
|
@ -25,7 +25,7 @@ $tracer_enabled = true;
|
||||||
$_tracer = new \EventTracer();
|
$_tracer = new \EventTracer();
|
||||||
$_tracer->begin("bootstrap");
|
$_tracer->begin("bootstrap");
|
||||||
_load_core_files();
|
_load_core_files();
|
||||||
$cache = new Cache(CACHE_DSN);
|
$cache = loadCache(CACHE_DSN);
|
||||||
$dsn = getenv("TEST_DSN");
|
$dsn = getenv("TEST_DSN");
|
||||||
$database = new Database($dsn ? $dsn : "sqlite::memory:");
|
$database = new Database($dsn ? $dsn : "sqlite::memory:");
|
||||||
create_dirs();
|
create_dirs();
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Themelet extends BaseThemelet
|
||||||
global $cache, $config;
|
global $cache, $config;
|
||||||
|
|
||||||
$cached = $cache->get("thumb-block:{$image->id}");
|
$cached = $cache->get("thumb-block:{$image->id}");
|
||||||
if ($cached) {
|
if(!is_null($cached)) {
|
||||||
return $cached;
|
return $cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in a new issue