mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
Various changes:
- Fix handling of binary data and long strings - Simplify handling of socket connections - Fix coverage
This commit is contained in:
parent
6ad3fb78a0
commit
e92bda5373
27 changed files with 93 additions and 52 deletions
|
@ -53,6 +53,8 @@ class Conf {
|
|||
public $dbMySQLPort = 3306;
|
||||
/** @var string Database name on MySQL/MariaDB database server (if using MySQL/MariaDB) */
|
||||
public $dbMySQLDb = "arsse";
|
||||
/** @var string Unix domain socket or named pipe to use for MySQL when not connecting over TCP */
|
||||
public $dbMySQLSocket = "";
|
||||
|
||||
/** @var string Class of the user management driver in use (Internal by default) */
|
||||
public $userDriver = User\Internal\Driver::class;
|
||||
|
|
|
@ -28,22 +28,14 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
if (!static::requirementsMet()) {
|
||||
throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore
|
||||
}
|
||||
$host = Arsse::$conf->dbMySQLHost;
|
||||
if ($host[0] === "/") {
|
||||
// host is a Unix socket
|
||||
$socket = $host;
|
||||
$host = "";
|
||||
} elseif(substr($host, 0, 9) === "\\\\.\\pipe\\") {
|
||||
// host is a Windows named piple
|
||||
$socket = substr($host, 10);
|
||||
$host = "";
|
||||
}
|
||||
$host = strtolower(!strlen((string) Arsse::$conf->dbMySQLHost) ? "localhost" : Arsse::$conf->dbMySQLHost);
|
||||
$socket = strlen((string) Arsse::$conf->dbMySQLSocket) ? Arsse::$conf->dbMySQLSocket : ini_get("mysqli.default_socket");
|
||||
$user = Arsse::$conf->dbMySQLUser ?? "";
|
||||
$pass = Arsse::$conf->dbMySQLPass ?? "";
|
||||
$port = Arsse::$conf->dbMySQLPost ?? 3306;
|
||||
$db = Arsse::$conf->dbMySQLDb ?? "arsse";
|
||||
// make the connection
|
||||
$this->makeConnection($user, $pass, $db, $host, $port, $socket ?? "");
|
||||
$this->makeConnection($user, $pass, $db, $host, $port, $socket);
|
||||
// set session variables
|
||||
foreach (static::makeSetupQueries() as $q) {
|
||||
$this->exec($q);
|
||||
|
|
|
@ -11,16 +11,12 @@ use JKingWeb\Arsse\Db\ExceptionInput;
|
|||
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
||||
|
||||
trait ExceptionBuilder {
|
||||
protected function buildException(): array {
|
||||
return self::buildEngineException($this->db->errno, $this->db->error);
|
||||
}
|
||||
|
||||
public static function buildEngineException($code, string $msg): array {
|
||||
switch ($code) {
|
||||
case 1205:
|
||||
return [ExceptionTimeout::class, 'general', $msg];
|
||||
case 1364:
|
||||
return [ExceptionInput::class, "constraintViolation", $msg];
|
||||
return [ExceptionInput::class, "engineConstraintViolation", $msg];
|
||||
case 1366:
|
||||
return [ExceptionInput::class, 'engineTypeViolation', $msg];
|
||||
default:
|
||||
|
|
|
@ -21,16 +21,13 @@ class PDODriver extends Driver {
|
|||
}
|
||||
|
||||
protected function makeConnection(string $db, string $user, string $password, string $host, int $port, string $socket) {
|
||||
$dsn = [];
|
||||
$dsn[] = "charset=utf8mb4";
|
||||
$dsn[] = "dbname=$db";
|
||||
if (strlen($host)) {
|
||||
$dsn[] = "host=$host";
|
||||
$dsn[] = "port=$port";
|
||||
} elseif (strlen($socket)) {
|
||||
$dsn[] = "socket=$socket";
|
||||
}
|
||||
$dsn = "mysql:".implode(";", $dsn);
|
||||
$dsn = "mysql:".implode(";", [
|
||||
"charset=utf8mb4",
|
||||
"dbname=$db",
|
||||
"host=$host",
|
||||
"socket=$socket",
|
||||
"port=$port",
|
||||
]);
|
||||
$this->db = new \PDO($dsn, $user, $password, [
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
|
|
@ -28,6 +28,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
|
|||
protected $packetSize;
|
||||
|
||||
protected $values;
|
||||
protected $longs;
|
||||
protected $binds = "";
|
||||
|
||||
public function __construct(\mysqli $db, string $query, array $bindings = [], int $packetSize = 4194304) {
|
||||
|
@ -39,9 +40,9 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
|
|||
|
||||
protected function prepare(string $query): bool {
|
||||
$this->st = $this->db->prepare($query);
|
||||
if (!$this->st) {
|
||||
list($excClass, $excMsg, $excData) = $this->buildEngineException($this->db->errno, $this->db->error);
|
||||
throw new $excClass($excMsg, $excData);
|
||||
if (!$this->st) { // @codeCoverageIgnore
|
||||
list($excClass, $excMsg, $excData) = $this->buildEngineException($this->db->errno, $this->db->error); // @codeCoverageIgnore
|
||||
throw new $excClass($excMsg, $excData); // @codeCoverageIgnore
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -56,16 +57,26 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
|
|||
|
||||
public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result {
|
||||
$this->st->reset();
|
||||
// clear normalized values
|
||||
$this->binds = "";
|
||||
$this->values = [];
|
||||
$this->longs = [];
|
||||
// prepare values and them all at once
|
||||
$this->bindValues($values);
|
||||
if ($this->values) {
|
||||
$this->st->bind_param($this->binds, ...$this->values);
|
||||
}
|
||||
// packetize any large values
|
||||
foreach ($this->longs as $pos => $data) {
|
||||
$this->st->send_long_data($pos, $data);
|
||||
unset($data);
|
||||
}
|
||||
// execute the statement
|
||||
$this->st->execute();
|
||||
// clear normalized values
|
||||
$this->binds = "";
|
||||
$this->values = [];
|
||||
$this->longs = [];
|
||||
// check for errors
|
||||
if ($this->st->sqlstate !== "00000") {
|
||||
if ($this->st->sqlstate === "HY000") {
|
||||
|
@ -85,14 +96,15 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
|
|||
protected function bindValue($value, string $type, int $position): bool {
|
||||
// this is a bit of a hack: we collect values (and MySQL bind types) here so that we can take
|
||||
// advantage of the work done by bindValues() even though MySQL requires everything to be bound
|
||||
// all at once; we also packetize large values here if necessary
|
||||
// all at once; we also segregate large values for later packetization
|
||||
if (($type === "binary" && !is_null($value)) || (is_string($value) && strlen($value) > $this->packetSize)) {
|
||||
$this->values[] = null;
|
||||
$this->st->send_long_data($position - 1, $value);
|
||||
$this->longs[$position - 1] = $value;
|
||||
$this->binds .= "b";
|
||||
} else {
|
||||
$this->values[] = $value;
|
||||
$this->binds .= self::BINDINGS[$type];
|
||||
}
|
||||
$this->binds .= self::BINDINGS[$type];
|
||||
return true;
|
||||
}
|
||||
public static function mungeQuery(string $query, array $types, ...$extraData): string {
|
||||
|
|
|
@ -17,6 +17,7 @@ trait Dispatch {
|
|||
}
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
public static function buildEngineException($code, string $msg): array {
|
||||
// PostgreSQL uses SQLSTATE exclusively, so this is not used
|
||||
return [];
|
||||
|
|
|
@ -11,6 +11,7 @@ class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement {
|
|||
return Statement::mungeQuery($query, $types, false);
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
public static function buildEngineException($code, string $msg): array {
|
||||
// PostgreSQL uses SQLSTATE exclusively, so this is not used
|
||||
return [];
|
||||
|
|
|
@ -20,7 +20,7 @@ trait SQLState {
|
|||
case "23000":
|
||||
case "23502":
|
||||
case "23505":
|
||||
return [ExceptionInput::class, "constraintViolation", $msg];
|
||||
return [ExceptionInput::class, "engineConstraintViolation", $msg];
|
||||
case "55P03":
|
||||
case "57014":
|
||||
return [ExceptionTimeout::class, 'general', $msg];
|
||||
|
|
|
@ -67,7 +67,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
/** @dataProvider provideBinaryBindings */
|
||||
public function testHandleBinaryData($value, string $type, string $exp) {
|
||||
if (in_array(static::$implementation, ["MySQL", "PostgreSQL", "PDO PostgreSQL"])) {
|
||||
if (in_array(static::$implementation, ["PostgreSQL", "PDO PostgreSQL"])) {
|
||||
$this->markTestSkipped("Correct handling of binary data with PostgreSQL and native MySQL is currently unknown");
|
||||
}
|
||||
if ($exp === "null") {
|
||||
|
|
|
@ -8,7 +8,9 @@ namespace JKingWeb\Arsse\TestCase\Db\MySQL;
|
|||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\Driver<extended> */
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\Driver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\ExceptionBuilder
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\MySQL;
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ use JKingWeb\Arsse\Test\DatabaseInformation;
|
|||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\Result<extended>
|
||||
*/
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\ExceptionBuilder
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\MySQL;
|
||||
|
||||
|
|
|
@ -8,7 +8,9 @@ namespace JKingWeb\Arsse\TestCase\Db\MySQL;
|
|||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\Statement<extended> */
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\Statement<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\ExceptionBuilder
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\MySQL;
|
||||
|
||||
|
@ -31,4 +33,16 @@ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
|
|||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function testBindLongString() {
|
||||
// this test requires some set-up to be effective
|
||||
static::$interface->query("CREATE TABLE arsse_test(`value` longtext not null) character set utf8mb4");
|
||||
// we'll use an unrealistic packet size of 1 byte to trigger special handling for strings which are too long for the maximum packet size
|
||||
$str = "long string";
|
||||
$s = new \JKingWeb\Arsse\Db\MySQL\Statement(static::$interface, "INSERT INTO arsse_test values(?)", ["str"], 1);
|
||||
$s->runArray([$str]);
|
||||
$s = new \JKingWeb\Arsse\Db\MySQL\Statement(static::$interface, "SELECT * from arsse_test", []);
|
||||
$val = $s->run()->getValue();
|
||||
$this->assertSame($str, $val);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,9 @@ namespace JKingWeb\Arsse\TestCase\Db\MySQL;
|
|||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\Driver<extended> */
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\Driver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\ExceptionBuilder
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\MySQL;
|
||||
|
||||
|
|
|
@ -9,8 +9,10 @@ namespace JKingWeb\Arsse\TestCase\Db\MySQLPDO;
|
|||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\PDODriver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\ExceptionBuilder
|
||||
* @covers \JKingWeb\Arsse\Db\PDODriver
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\MySQLPDO;
|
||||
|
||||
|
|
|
@ -11,7 +11,9 @@ use JKingWeb\Arsse\Test\DatabaseInformation;
|
|||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PDOResult<extended>
|
||||
*/
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\ExceptionBuilder
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\MySQLPDO;
|
||||
|
||||
|
|
|
@ -9,7 +9,9 @@ namespace JKingWeb\Arsse\TestCase\Db\MySQLPDO;
|
|||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\PDOStatement<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\ExceptionBuilder
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\MySQLPDO;
|
||||
|
||||
|
|
|
@ -9,7 +9,10 @@ namespace JKingWeb\Arsse\TestCase\Db\MySQLPDO;
|
|||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\PDODriver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\ExceptionBuilder
|
||||
* @covers \JKingWeb\Arsse\Db\PDODriver
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\MySQLPDO;
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
|
|||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Dispatch<extended> */
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Dispatch<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\PostgreSQL;
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
|
|||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Statement<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Dispatch<extended> */
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Dispatch<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\PostgreSQL;
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
|
|||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver<extended> */
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\PostgreSQL;
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
|
|||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDODriver
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\PostgreSQLPDO;
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
|
|||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\PDOStatement<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\PostgreSQLPDO;
|
||||
|
||||
|
|
|
@ -9,7 +9,9 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
|
|||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
* @covers \JKingWeb\Arsse\Db\PDODriver
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\PostgreSQLPDO;
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@ use Phake;
|
|||
/**
|
||||
* @covers \JKingWeb\Arsse\Db\SQLite3\PDODriver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDODriver
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
protected $data;
|
||||
protected $drv;
|
||||
|
|
|
@ -9,7 +9,8 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO;
|
|||
/**
|
||||
* @covers \JKingWeb\Arsse\Db\SQLite3\PDODriver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDODriver
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\SQLite3PDO;
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO;
|
|||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Db\PDOStatement<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\SQLite3PDO;
|
||||
|
||||
|
|
|
@ -8,7 +8,9 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO;
|
|||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Db\SQLite3\PDODriver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
* @covers \JKingWeb\Arsse\Db\PDODriver
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError
|
||||
* @covers \JKingWeb\Arsse\Db\SQLState */
|
||||
class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate {
|
||||
use \JKingWeb\Arsse\TestCase\DatabaseDrivers\SQLite3PDO;
|
||||
|
||||
|
|
Loading…
Reference in a new issue