230 lines
8.9 KiB
PHP
230 lines
8.9 KiB
PHP
<?php
|
|
|
|
namespace php;
|
|
|
|
use Composer\Semver\Comparator;
|
|
use Monolog\Logger;
|
|
use PDO;
|
|
|
|
|
|
/**
|
|
* Helper class for interacting with the database.
|
|
*/
|
|
class Database
|
|
{
|
|
/**
|
|
* The currently running software version.
|
|
*/
|
|
public const LATEST_VERSION = "%%DB_VERSION_NUMBER%%";
|
|
|
|
/**
|
|
* @var Logger The logger to use for logging.
|
|
*/
|
|
private Logger $logger;
|
|
/**
|
|
* @var PDO The PDO object that connects to the database.
|
|
*/
|
|
public PDO $conn;
|
|
|
|
|
|
/**
|
|
* Opens a connection with the database at `filename`.
|
|
*
|
|
* @param Logger $logger the logger to use for logging
|
|
* @param string $filename the path to the database to connect to
|
|
*/
|
|
public function __construct(Logger $logger, string $filename)
|
|
{
|
|
$this->logger = $logger;
|
|
|
|
$this->conn = new PDO("sqlite:$filename", options: array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
|
|
$this->conn->exec("PRAGMA foreign_keys = ON;");
|
|
}
|
|
|
|
|
|
/**
|
|
* Installs all necessary data structures to get the database working.
|
|
*
|
|
* @param Mailer $mailer the mailer to install
|
|
* @param UserManager $userManager the use manager to install
|
|
* @param TrackingManager $trackingManager the tracking manager to install
|
|
* @return void
|
|
*/
|
|
public function auto_install(Mailer $mailer, UserManager $userManager, TrackingManager $trackingManager): void
|
|
{
|
|
// TODO: Keep track of statistics, such as #users, mails sent, etc.
|
|
self::transaction($this->conn, function () use ($mailer, $userManager, $trackingManager) {
|
|
// Check if already installed
|
|
$stmt = $this->conn->prepare("SELECT count(*) FROM sqlite_master WHERE type = 'table';");
|
|
$stmt->execute();
|
|
if ($stmt->fetch()[0] !== 0) return Response::satisfied();
|
|
|
|
$this->logger->info("Database does not exist. Installing new database.");
|
|
|
|
// Create `meta` table
|
|
$this->conn->exec("CREATE TABLE meta(k TEXT NOT NULL UNIQUE PRIMARY KEY, v TEXT);");
|
|
$stmt = $this->conn->prepare("INSERT INTO meta (k, v) VALUES ('version', :version);");
|
|
$stmt->bindValue(":version", Database::LATEST_VERSION);
|
|
$stmt->execute();
|
|
|
|
// Create other tables
|
|
$mailer->install();
|
|
$userManager->install();
|
|
$trackingManager->install();
|
|
|
|
$this->logger->info("Installation complete.");
|
|
return Response::satisfied();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Automatically migrates the database to the latest version.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function auto_migrate(): void
|
|
{
|
|
self::transaction($this->conn, function () {
|
|
// Check if migration is necessary
|
|
$stmt = $this->conn->prepare("SELECT v FROM meta WHERE k='version';");
|
|
$stmt->execute();
|
|
$db_version = $stmt->fetchAll(PDO::FETCH_ASSOC)[0]["v"];
|
|
if (Comparator::greaterThanOrEqualTo($db_version, Database::LATEST_VERSION))
|
|
return Response::satisfied();
|
|
|
|
$this->logger->info("Current db is v$db_version. Will migrate to v" . Database::LATEST_VERSION . ".");
|
|
|
|
// Get current version
|
|
$stmt = $this->conn->prepare("SELECT v FROM meta WHERE k='version';");
|
|
$stmt->execute();
|
|
$db_version = $stmt->fetchAll(PDO::FETCH_ASSOC)[0]["v"];
|
|
|
|
// Execute migration code
|
|
if (Comparator::lessThan($db_version, "0.5.0")) self::migrate_0_5_0();
|
|
if (Comparator::lessThan($db_version, "0.8.0")) self::migrate_0_8_0();
|
|
if (Comparator::lessThan($db_version, "0.10.0")) self::migrate_0_10_0();
|
|
|
|
// Update version
|
|
$stmt = $this->conn->prepare("UPDATE meta SET v=:version WHERE k='version';");
|
|
$stmt->bindValue(":version", Database::LATEST_VERSION);
|
|
$stmt->execute();
|
|
|
|
$this->logger->info("Completed migration to v" . Database::LATEST_VERSION . ".");
|
|
|
|
return Response::satisfied();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Migrates the database from a previous version to one compatible with v0.5.0.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function migrate_0_5_0(): void
|
|
{
|
|
$this->logger->info("Migrating to v0.5.0.");
|
|
|
|
$res = $this->conn->exec("ALTER TABLE users
|
|
ADD COLUMN email_notifications_enabled INT NOT NULL DEFAULT(1);") !== false;
|
|
|
|
if (!$res) {
|
|
$this->logger->error("Failed migrating to v0.5.0.", ["error" => $this->conn->errorInfo()]);
|
|
$this->conn->rollBack();
|
|
Util::http_exit(500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrates the database from a previous version to one compatible with v0.8.0.
|
|
*
|
|
* @return void
|
|
* @noinspection SqlResolve Function necessarily refers to old scheme which is not detected by tools
|
|
*/
|
|
private function migrate_0_8_0(): void
|
|
{
|
|
$this->logger->info("Migrating to v0.8.0.");
|
|
|
|
$res =
|
|
$this->conn->exec("CREATE TABLE new_email_tasks(type TEXT NOT NULL,
|
|
recipient TEXT NOT NULL,
|
|
arg1 TEXT DEFAULT(NULL),
|
|
PRIMARY KEY (type, recipient, arg1));") !== false &&
|
|
$this->conn->exec("INSERT INTO new_email_tasks (type, recipient, arg1)
|
|
SELECT type, arg1, arg2
|
|
FROM email_tasks WHERE arg1 NOT NULL;") !== false &&
|
|
$this->conn->exec("DROP TABLE email_tasks;") !== false &&
|
|
$this->conn->exec("ALTER TABLE new_email_tasks RENAME TO email_tasks;") !== false;
|
|
|
|
if (!$res) {
|
|
$this->logger->error("Failed migrating to v0.8.0.", ["error" => $this->conn->errorInfo()]);
|
|
$this->conn->rollBack();
|
|
Util::http_exit(500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrates the database from a previous version to one compatible with v0.10.0.
|
|
*
|
|
* @return void
|
|
* @noinspection SqlResolve Function necessarily refers to old scheme which is not detected by tools
|
|
*/
|
|
private function migrate_0_10_0(): void
|
|
{
|
|
$this->logger->info("Migrating to v0.10.0.");
|
|
|
|
$res =
|
|
$this->conn->exec("CREATE TABLE new_email_tasks(type TEXT NOT NULL,
|
|
recipient TEXT NOT NULL,
|
|
arg1 TEXT NOT NULL DEFAULT(''),
|
|
arg2 TEXT NOT NULL DEFAULT(''),
|
|
PRIMARY KEY (type, recipient, arg1, arg2));") !== false &&
|
|
$this->conn->exec("INSERT INTO new_email_tasks (type, recipient, arg1)
|
|
SELECT type, recipient, arg1
|
|
FROM email_tasks;") !== false &&
|
|
$this->conn->exec("DROP TABLE email_tasks;") !== false &&
|
|
$this->conn->exec("ALTER TABLE new_email_tasks RENAME TO email_tasks;") !== false &&
|
|
$this->conn->exec("UPDATE email_tasks
|
|
SET type='notify-status-changed' AND arg2='dead'
|
|
WHERE type='notify-death'") !== false;
|
|
|
|
if (!$res) {
|
|
$this->logger->error("Failed migrating to v0.10.0.", ["error" => $this->conn->errorInfo()]);
|
|
$this->conn->rollBack();
|
|
Util::http_exit(500);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Executes `lambda` within a single transaction, allowing nesting.
|
|
*
|
|
* If no transaction has been started when this function is invoked, a new transaction is started. If `lambda`
|
|
* returns a satisfied `Response`, the transaction is committed, otherwise the transaction is rolled back.
|
|
*
|
|
* If a transaction has been started when this function is invoked, the transaction is continued, and `lambda` does
|
|
* not by itself commit or roll back the transaction, this function does not commit or roll back the transaction
|
|
* either.
|
|
*
|
|
* @param PDO $conn the connection to perform the transaction over
|
|
* @param callable $lambda the function to execute within the transaction, returning a `Response` indicating whether
|
|
* the transaction should be committed or rolled back, or returning `null` if the transaction should be committed
|
|
* @return Response the `Response` returned by `lambda`, or a satisfied `Response` if `lambda` returns `void` or
|
|
* `null`
|
|
*/
|
|
public static function transaction(PDO $conn, callable $lambda): Response
|
|
{
|
|
$initially_in_transaction = $conn->inTransaction();
|
|
|
|
if (!$initially_in_transaction)
|
|
$conn->beginTransaction();
|
|
|
|
$output = $lambda() ?? Response::satisfied();
|
|
|
|
if ($conn->inTransaction() && !$initially_in_transaction)
|
|
if ($output->satisfied) $conn->commit();
|
|
else $conn->rollBack();
|
|
|
|
return $output;
|
|
}
|
|
}
|