Revamp email system, config, and db exceptions
This commit is contained in:
parent
19922907c8
commit
5898c95709
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "fwdekker/death-notifier",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"version": "0.15.6", "_comment_version": "Also update version in `package.json`!",
|
||||
"version": "0.16.0", "_comment_version": "Also update version in `package.json`!",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"homepage": "https://git.fwdekker.com/tools/death-notifier",
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "death-notifier",
|
||||
"version": "0.15.6", "_comment_version": "Also update version in `composer.json`!",
|
||||
"version": "0.16.0", "_comment_version": "Also update version in `composer.json`!",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"author": "Florine W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
use com\fwdekker\deathnotifier\ActionDispatcher;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\EmulateCronAction;
|
||||
use com\fwdekker\deathnotifier\LoggerUtil;
|
||||
use com\fwdekker\deathnotifier\mailer\MailManager;
|
||||
use com\fwdekker\deathnotifier\mailer\ProcessEmailQueueAction;
|
||||
use com\fwdekker\deathnotifier\mediawiki\MediaWiki;
|
||||
use com\fwdekker\deathnotifier\ProtectedAction;
|
||||
use com\fwdekker\deathnotifier\Response;
|
||||
use com\fwdekker\deathnotifier\StartSessionAction;
|
||||
use com\fwdekker\deathnotifier\tracking\AddTrackingAction;
|
||||
|
@ -37,18 +37,15 @@ require_once __DIR__ . "/.vendor/autoload.php";
|
|||
|
||||
|
||||
// Preamble
|
||||
$config = Util::read_config();
|
||||
|
||||
LoggerUtil::configure($config["logger"]);
|
||||
$logger = LoggerUtil::with_name();
|
||||
|
||||
$config = Config::get();
|
||||
if (hash_equals($config["admin"]["cli_secret"], "REPLACE THIS WITH A SECRET VALUE")) {
|
||||
$logger->error("You must set a CLI secret in the configuration file before running Death Notifier.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$db = new Database($config["database"]["filename"]);
|
||||
|
||||
$mediawiki = new MediaWiki();
|
||||
$mail_manager = new MailManager($db->conn);
|
||||
$user_manager = new UserManager($db->conn, $mail_manager);
|
||||
|
@ -88,8 +85,8 @@ try {
|
|||
$dispatcher->register_action(new RemoveTrackingAction($tracking_manager));
|
||||
// CLI actions
|
||||
$cli_actions = [
|
||||
new UpdateTrackingsAction($db->conn, $tracking_manager, $mediawiki, $mail_manager, $config["admin"]["cli_secret"]),
|
||||
new ProcessEmailQueueAction($config, $mail_manager, $config["admin"]["cli_secret"]),
|
||||
new UpdateTrackingsAction($db->conn, $tracking_manager, $mediawiki, $mail_manager),
|
||||
new ProcessEmailQueueAction($mail_manager),
|
||||
];
|
||||
$dispatcher->register_action($cli_actions[0]);
|
||||
$dispatcher->register_action($cli_actions[1]);
|
||||
|
@ -114,5 +111,5 @@ header("Content-type:application/json;charset=utf-8");
|
|||
exit(json_encode([
|
||||
"payload" => $response->payload,
|
||||
"satisfied" => $response->satisfied,
|
||||
"token" => $_SESSION["token"]
|
||||
"token" => $_SESSION["token"],
|
||||
]));
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
|
||||
/**
|
||||
* The application's configuration, read lazily from configuration files.
|
||||
*
|
||||
* Contains global state, but that's fine since it's read-only.
|
||||
*/
|
||||
class Config
|
||||
{
|
||||
/**
|
||||
* @var array<string, mixed>|null the application's configuration, or `null` if it has not been read yet
|
||||
*/
|
||||
private static ?array $config = null;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the application's configuration.
|
||||
*
|
||||
* @return array<string, mixed> the application's configuration
|
||||
*/
|
||||
public static function get(): array
|
||||
{
|
||||
if (self::$config === null)
|
||||
self::$config = self::read_config();
|
||||
|
||||
return self::$config;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads the configuration file and overrides it with the user's custom values.
|
||||
*
|
||||
* @return array<string, mixed> the application's configuration
|
||||
*/
|
||||
private static function read_config(): array
|
||||
{
|
||||
// TODO: Check permissions, return `null` if too permissive
|
||||
$config = parse_ini_file("config.default.ini.php", process_sections: true, scanner_mode: INI_SCANNER_TYPED);
|
||||
if ($config === false) throw new InvalidArgumentException("Invalid `config.default.ini.php` file.");
|
||||
|
||||
if (file_exists("config.ini.php")) {
|
||||
$config_custom = parse_ini_file("config.ini.php", process_sections: true, scanner_mode: INI_SCANNER_TYPED);
|
||||
if ($config_custom === false) throw new InvalidArgumentException("Invalid `config.ini.php` file.");
|
||||
|
||||
$config = array_replace_recursive($config, $config_custom);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@ use com\fwdekker\deathnotifier\mailer\MailManager;
|
|||
use com\fwdekker\deathnotifier\tracking\TrackingManager;
|
||||
use com\fwdekker\deathnotifier\user\UserManager;
|
||||
use Composer\Semver\Comparator;
|
||||
use Error;
|
||||
use Exception;
|
||||
use Monolog\Logger;
|
||||
use PDO;
|
||||
|
||||
|
@ -47,19 +49,20 @@ class Database
|
|||
/**
|
||||
* Installs all necessary data structures to get the database working.
|
||||
*
|
||||
* @param MailManager $mailer the mailer to install
|
||||
* @param UserManager $userManager the use manager to install
|
||||
* @param TrackingManager $trackingManager the tracking manager to install
|
||||
* @param MailManager $mail_manager the mail manager to install
|
||||
* @param UserManager $user_manager the user manager to install
|
||||
* @param TrackingManager $tracking_manager the tracking manager to install
|
||||
* @return void
|
||||
*/
|
||||
public function auto_install(MailManager $mailer, UserManager $userManager, TrackingManager $trackingManager): void
|
||||
public function auto_install(MailManager $mail_manager, UserManager $user_manager,
|
||||
TrackingManager $tracking_manager): void
|
||||
{
|
||||
// TODO: Keep track of statistics, such as #users, mails sent, etc.
|
||||
self::transaction($this->conn, function () use ($mailer, $userManager, $trackingManager) {
|
||||
self::transaction($this->conn, function () use ($mail_manager, $user_manager, $tracking_manager) {
|
||||
// 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();
|
||||
if ($stmt->fetch()[0] !== 0)
|
||||
return;
|
||||
|
||||
$this->logger->info("Database does not exist. Installing new database.");
|
||||
|
||||
|
@ -70,12 +73,11 @@ class Database
|
|||
$stmt->execute();
|
||||
|
||||
// Create other tables
|
||||
$mailer->install();
|
||||
$userManager->install();
|
||||
$trackingManager->install();
|
||||
$mail_manager->install();
|
||||
$user_manager->install();
|
||||
$tracking_manager->install();
|
||||
|
||||
$this->logger->info("Installation complete.");
|
||||
return Response::satisfied();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -92,7 +94,7 @@ class Database
|
|||
$stmt->execute();
|
||||
$db_version = $stmt->fetchAll(PDO::FETCH_ASSOC)[0]["v"];
|
||||
if (Comparator::greaterThanOrEqualTo($db_version, Database::LATEST_VERSION))
|
||||
return Response::satisfied();
|
||||
return;
|
||||
|
||||
$this->logger->info("Current db is v$db_version. Will migrate to v" . Database::LATEST_VERSION . ".");
|
||||
|
||||
|
@ -105,6 +107,7 @@ class Database
|
|||
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();
|
||||
if (Comparator::lessThan($db_version, "0.16.0")) self::migrate_0_16_0();
|
||||
|
||||
// Update version
|
||||
$stmt = $this->conn->prepare("UPDATE meta SET v=:version WHERE k='version';");
|
||||
|
@ -112,11 +115,10 @@ class Database
|
|||
$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.
|
||||
*
|
||||
|
@ -126,14 +128,8 @@ class Database
|
|||
{
|
||||
$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);
|
||||
}
|
||||
$this->conn->exec("ALTER TABLE users
|
||||
ADD COLUMN email_notifications_enabled INT NOT NULL DEFAULT(1);");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -146,22 +142,15 @@ class Database
|
|||
{
|
||||
$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);
|
||||
}
|
||||
$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));");
|
||||
$this->conn->exec("INSERT INTO new_email_tasks (type, recipient, arg1)
|
||||
SELECT type, arg1, arg2
|
||||
FROM email_tasks WHERE arg1 NOT NULL;");
|
||||
$this->conn->exec("DROP TABLE email_tasks;");
|
||||
$this->conn->exec("ALTER TABLE new_email_tasks RENAME TO email_tasks;");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -174,59 +163,67 @@ class Database
|
|||
{
|
||||
$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;
|
||||
$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));");
|
||||
$this->conn->exec("INSERT INTO new_email_tasks (type, recipient, arg1)
|
||||
SELECT type, recipient, arg1
|
||||
FROM email_tasks;");
|
||||
$this->conn->exec("DROP TABLE email_tasks;");
|
||||
$this->conn->exec("ALTER TABLE new_email_tasks RENAME TO email_tasks;");
|
||||
$this->conn->exec("UPDATE email_tasks
|
||||
SET type='notify-status-changed' AND arg2='dead'
|
||||
WHERE type='notify-death'");
|
||||
}
|
||||
|
||||
if (!$res) {
|
||||
$this->logger->error("Failed migrating to v0.10.0.", ["error" => $this->conn->errorInfo()]);
|
||||
$this->conn->rollBack();
|
||||
Util::http_exit(500);
|
||||
}
|
||||
/**
|
||||
* Migrates the database from a previous version to one compatible with v0.16.0.
|
||||
*
|
||||
* @return void
|
||||
* @noinspection SqlResolve Function necessarily refers to old schema which is not detected by tools
|
||||
*/
|
||||
private function migrate_0_16_0(): void
|
||||
{
|
||||
$this->logger->info("Migrating to v0.16.0.");
|
||||
|
||||
$this->conn->exec("DROP TABLE email_tasks;");
|
||||
$this->conn->exec("CREATE TABLE email_tasks(type_key TEXT NOT NULL,
|
||||
recipient TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
PRIMARY KEY (type_key, recipient));");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes `lambda` within a single transaction, allowing nesting.
|
||||
* Executes `lambda` within a 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.
|
||||
* throws an exception, the transaction is rolled back, otherwise, if this method is not invoked while a transaction
|
||||
* was already ongoing, the transaction is committed.
|
||||
*
|
||||
* @param PDO $conn the connection to perform the transaction over
|
||||
* @param callable(): (Response|null) $lambda the function to execute within the transaction, returning a satisfied
|
||||
* `Response` or `null` if the transaction should be committed, or an unsatisfied `Response` if the transaction
|
||||
* should be rolled back
|
||||
* @return Response the `Response` returned by `lambda`, or a satisfied `Response` if `lambda` returns `void` or
|
||||
* `null`
|
||||
* @param callable(): void $lambda the function to execute within a transaction
|
||||
*/
|
||||
public static function transaction(PDO $conn, callable $lambda): Response
|
||||
public static function transaction(PDO $conn, callable $lambda): void
|
||||
{
|
||||
$initially_in_transaction = $conn->inTransaction();
|
||||
|
||||
if (!$initially_in_transaction)
|
||||
$conn->beginTransaction();
|
||||
|
||||
$output = $lambda() ?? Response::satisfied();
|
||||
try {
|
||||
$lambda();
|
||||
} catch (Error $exception) {
|
||||
if ($conn->inTransaction())
|
||||
$conn->rollBack();
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
if ($conn->inTransaction() && !$initially_in_transaction)
|
||||
if ($output->satisfied) $conn->commit();
|
||||
else $conn->rollBack();
|
||||
|
||||
return $output;
|
||||
$conn->commit();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,27 +16,12 @@ use Monolog\Logger;
|
|||
class LoggerUtil
|
||||
{
|
||||
/**
|
||||
* @var Logger|null the main logger instance that other loggers are derived from
|
||||
* @var Logger|null the main logger instance that other loggers are derived from, or `null` if the main logger has
|
||||
* not been created yet
|
||||
*/
|
||||
private static ?Logger $main_logger = null;
|
||||
|
||||
|
||||
/**
|
||||
* Configures the main logger.
|
||||
*
|
||||
* @param array<string, mixed> $config the logger configuration
|
||||
* @return void
|
||||
*/
|
||||
public static function configure(array $config): void
|
||||
{
|
||||
if (self::$main_logger !== null)
|
||||
throw new IllegalStateException("Logger has already been created.");
|
||||
|
||||
self::$main_logger = new Logger("main");
|
||||
self::$main_logger->pushHandler(new StreamHandler($config["file"], $config["level"]));
|
||||
ErrorHandler::register(self::$main_logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a logger with the given name.
|
||||
*
|
||||
|
@ -45,8 +30,13 @@ class LoggerUtil
|
|||
*/
|
||||
public static function with_name(string $name = "main"): Logger
|
||||
{
|
||||
if (self::$main_logger === null)
|
||||
throw new IllegalStateException("Logger has not been created yet.");
|
||||
if (self::$main_logger === null) {
|
||||
$config = Config::get();
|
||||
|
||||
self::$main_logger = new Logger("main");
|
||||
self::$main_logger->pushHandler(new StreamHandler($config["file"], $config["level"]));
|
||||
ErrorHandler::register(self::$main_logger);
|
||||
}
|
||||
|
||||
return self::$main_logger->withName($name);
|
||||
}
|
||||
|
|
|
@ -27,27 +27,6 @@ class Util
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads the configuration file and overrides it with the user's custom values.
|
||||
*
|
||||
* @return array<string, array<string, mixed>> the configuration
|
||||
*/
|
||||
static function read_config(): array
|
||||
{
|
||||
// TODO: Check permissions, return `null` if too permissive
|
||||
$config = parse_ini_file("config.default.ini.php", process_sections: true, scanner_mode: INI_SCANNER_TYPED);
|
||||
if ($config === false) throw new InvalidArgumentException("Invalid `config.default.ini.php` file.");
|
||||
|
||||
if (file_exists("config.ini.php")) {
|
||||
$config_custom = parse_ini_file("config.ini.php", process_sections: true, scanner_mode: INI_SCANNER_TYPED);
|
||||
if ($config_custom === false) throw new InvalidArgumentException("Invalid `config.ini.php` file.");
|
||||
|
||||
$config = array_replace_recursive($config, $config_custom);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses POST values from JSON-based inputs.
|
||||
*
|
||||
|
|
|
@ -11,32 +11,36 @@ use com\fwdekker\deathnotifier\user\ChangedPasswordEmail;
|
|||
use com\fwdekker\deathnotifier\user\RegisterEmail;
|
||||
use com\fwdekker\deathnotifier\user\ResetPasswordEmail;
|
||||
use com\fwdekker\deathnotifier\user\VerifyEmailEmail;
|
||||
use JsonSerializable;
|
||||
|
||||
|
||||
/**
|
||||
* A serializable email that can be queued in a database and can be sent.
|
||||
*
|
||||
* When serialized, an email is represented by the type of email and two arguments. When deserialized, an email can be
|
||||
* constructed from those arguments, and the implementation returns a specific subject and message.
|
||||
* An email that can be queued in a database and then sent.
|
||||
*/
|
||||
abstract class Email
|
||||
{
|
||||
/**
|
||||
* @var string a string identifying the type of email
|
||||
* @var string a string identifying the type of email and distinguishing it from similar instances of the same type
|
||||
*/
|
||||
public string $type;
|
||||
public readonly string $type_key;
|
||||
/**
|
||||
* @var string the intended recipient of the email
|
||||
*/
|
||||
public string $recipient;
|
||||
public readonly string $recipient;
|
||||
|
||||
|
||||
/**
|
||||
* @var string the first argument to construct the email
|
||||
* Constructs a new email.
|
||||
*
|
||||
* @param string $type_key a string identifying the type of email and distinguishing it from similar instances of
|
||||
* the same type
|
||||
* @param string $recipient the intended recipient of the email
|
||||
*/
|
||||
public string $arg1 = "";
|
||||
/**
|
||||
* @var string the second argument to construct the email
|
||||
*/
|
||||
public string $arg2 = "";
|
||||
public function __construct(string $type_key, string $recipient)
|
||||
{
|
||||
$this->type_key = $type_key;
|
||||
$this->recipient = $recipient;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
@ -44,42 +48,12 @@ abstract class Email
|
|||
*
|
||||
* @return string the subject header of the email
|
||||
*/
|
||||
public abstract function getSubject(): string;
|
||||
public abstract function get_subject(): string;
|
||||
|
||||
/**
|
||||
* Returns the body of the email.
|
||||
*
|
||||
* @param array<string, array<string, mixed>> $config the software configuration
|
||||
* @return string the body of the email
|
||||
*/
|
||||
public abstract function getBody(array $config): string;
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes an email back into an instance.
|
||||
*
|
||||
* Only types of emails that are known can be deserialized.
|
||||
*
|
||||
* @param string $type the type of email to deserialize
|
||||
* @param string $recipient the intended recipient of the email
|
||||
* @param string $arg1 the first argument to construct the email
|
||||
* @param string $arg2 the second argument to construct the email
|
||||
* @return Email a deserialized email
|
||||
*/
|
||||
public static function deserialize(string $type, string $recipient, string $arg1, string $arg2): Email
|
||||
{
|
||||
// TODO: Dynamically instantiate class from class name
|
||||
// TODO: Add serialize and deserialize functions, implemented by each subclass
|
||||
return match ($type) {
|
||||
RegisterEmail::TYPE => new RegisterEmail($recipient, $arg1),
|
||||
VerifyEmailEmail::TYPE => new VerifyEmailEmail($recipient, $arg1),
|
||||
ChangedEmailEmail::TYPE => new ChangedEmailEmail($recipient, $arg1),
|
||||
ChangedPasswordEmail::TYPE => new ChangedPasswordEmail($recipient),
|
||||
ResetPasswordEmail::TYPE => new ResetPasswordEmail($recipient, $arg1),
|
||||
NotifyArticleDeletedEmail::TYPE => new NotifyArticleDeletedEmail($recipient, $arg1),
|
||||
NotifyArticleUndeletedEmail::TYPE => new NotifyArticleUndeletedEmail($recipient, $arg1),
|
||||
NotifyStatusChangedEmail::TYPE => new NotifyStatusChangedEmail($recipient, $arg1, $arg2),
|
||||
default => throw new IllegalStateException("Unknown email type $type."),
|
||||
};
|
||||
}
|
||||
public abstract function get_body(): string;
|
||||
}
|
||||
|
|
|
@ -34,11 +34,11 @@ class MailManager
|
|||
*/
|
||||
public function install(): void
|
||||
{
|
||||
$this->conn->exec("CREATE TABLE email_tasks(type TEXT NOT NULL,
|
||||
$this->conn->exec("CREATE TABLE email_tasks(type_key TEXT NOT NULL,
|
||||
recipient TEXT NOT NULL,
|
||||
arg1 TEXT NOT NULL DEFAULT(''),
|
||||
arg2 TEXT NOT NULL DEFAULT(''),
|
||||
PRIMARY KEY (type, recipient, arg1, arg2));");
|
||||
subject TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
PRIMARY KEY (type_key, recipient));");
|
||||
}
|
||||
|
||||
|
||||
|
@ -50,53 +50,42 @@ class MailManager
|
|||
*/
|
||||
public function queue_email(Email $email): void
|
||||
{
|
||||
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO email_tasks (type, recipient, arg1, arg2)
|
||||
VALUES (:type, :recipient, :arg1, :arg2);");
|
||||
$stmt->bindValue(":type", $email->type);
|
||||
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO email_tasks (type_key, recipient, subject, body)
|
||||
VALUES (:type_key, :recipient, :subject, :body);");
|
||||
$stmt->bindValue(":type_key", $email->type_key);
|
||||
$stmt->bindValue(":recipient", $email->recipient);
|
||||
$stmt->bindValue(":arg1", $email->arg1);
|
||||
$stmt->bindValue(":arg2", $email->arg2);
|
||||
$stmt->bindValue(":subject", $email->get_subject());
|
||||
$stmt->bindValue(":body", $email->get_body());
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all queued emails.
|
||||
*
|
||||
* @return array<Email> the queued emails
|
||||
* @return array<array{"type_key": string, "recipient": string, "subject": string, "body": string}> the queued
|
||||
* emails
|
||||
*/
|
||||
public function get_queue(): array
|
||||
{
|
||||
$stmt = $this->conn->prepare("SELECT type, recipient, arg1, arg2 FROM email_tasks;");
|
||||
$stmt = $this->conn->prepare("SELECT type_key, recipient, subject, body FROM email_tasks;");
|
||||
$stmt->execute();
|
||||
$emails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return array_map(
|
||||
fn($row) => Email::deserialize($row["type"], $row["recipient"], $row["arg1"], $row["arg2"]),
|
||||
$emails
|
||||
);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes mails from the queue.
|
||||
*
|
||||
* @param array<Email> $emails the emails to remove from the queue
|
||||
* @param array<array{"type_key": string, "recipient": string}> $emails the emails to remove from the queue
|
||||
* @return void
|
||||
*/
|
||||
public function unqueue_emails(array $emails): void
|
||||
{
|
||||
$stmt = $this->conn->prepare("DELETE FROM email_tasks
|
||||
WHERE type=:type AND recipient=:recipient AND arg1=:arg1 AND arg2=:arg2;");
|
||||
$stmt->bindParam(":type", $type);
|
||||
WHERE type_key=:type_key AND recipient=:recipient;");
|
||||
$stmt->bindParam(":type_key", $type_key);
|
||||
$stmt->bindParam(":recipient", $recipient);
|
||||
$stmt->bindParam(":arg1", $arg1);
|
||||
$stmt->bindParam(":arg2", $arg2);
|
||||
|
||||
foreach ($emails as $email) {
|
||||
$type = $email->type;
|
||||
$recipient = $email->recipient;
|
||||
$arg1 = $email->arg1;
|
||||
$arg2 = $email->arg2;
|
||||
|
||||
foreach ($emails as ["type_key" => $type_key, "recipient" => $recipient])
|
||||
$stmt->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace com\fwdekker\deathnotifier\mailer;
|
|||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionException;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\LoggerUtil;
|
||||
use com\fwdekker\deathnotifier\validator\IsEqualToRule;
|
||||
use Monolog\Logger;
|
||||
|
@ -22,10 +23,6 @@ class ProcessEmailQueueAction extends Action
|
|||
* @var Logger the logger to log with
|
||||
*/
|
||||
private readonly Logger $logger;
|
||||
/**
|
||||
* @var array<string, mixed> the application's configuration
|
||||
*/
|
||||
private readonly array $config;
|
||||
/**
|
||||
* @var MailManager the manager to process the queue with
|
||||
*/
|
||||
|
@ -35,20 +32,19 @@ class ProcessEmailQueueAction extends Action
|
|||
/**
|
||||
* Constructs a new `ProcessEmailQueueAction`.
|
||||
*
|
||||
* @param array<string, mixed> $config the application's configuration
|
||||
* @param MailManager $mail_manager the manager to process the queue with
|
||||
* @param string $password the admin password required to perform this function
|
||||
*/
|
||||
public function __construct(array $config, MailManager $mail_manager, string $password)
|
||||
public function __construct(MailManager $mail_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::CLI,
|
||||
"process-email-queue",
|
||||
rule_lists: ["password" => [new IsEqualToRule($password, "Incorrect password.")]],
|
||||
rule_lists: [
|
||||
"password" => [new IsEqualToRule(Config::get()["admin"]["cli_secret"], "Incorrect password.")]
|
||||
],
|
||||
);
|
||||
|
||||
$this->logger = LoggerUtil::with_name($this::class);
|
||||
$this->config = $config;
|
||||
$this->mail_manager = $mail_manager;
|
||||
}
|
||||
|
||||
|
@ -65,11 +61,11 @@ class ProcessEmailQueueAction extends Action
|
|||
|
||||
$mailer = $this->create_mailer();
|
||||
foreach ($emails as $email) {
|
||||
$mailer->Subject = $email->getSubject();
|
||||
$mailer->Body = $email->getBody($this->config);
|
||||
$mailer->Subject = $email["subject"];
|
||||
$mailer->Body = $email["body"];
|
||||
|
||||
try {
|
||||
$mailer->addAddress($email->recipient);
|
||||
$mailer->addAddress($email["recipient"]);
|
||||
$mailer->send();
|
||||
} catch (PHPMailerException $exception) {
|
||||
$mailer->getSMTPInstance()->reset();
|
||||
|
@ -94,6 +90,8 @@ class ProcessEmailQueueAction extends Action
|
|||
*/
|
||||
private function create_mailer(): PHPMailer
|
||||
{
|
||||
$config = Config::get();
|
||||
|
||||
$mailer = new PHPMailer();
|
||||
$mailer->IsSMTP();
|
||||
$mailer->CharSet = "UTF-8";
|
||||
|
@ -102,12 +100,12 @@ class ProcessEmailQueueAction extends Action
|
|||
$mailer->SMTPDebug = SMTP::DEBUG_OFF;
|
||||
$mailer->SMTPKeepAlive = true;
|
||||
$mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
||||
$mailer->Host = $this->config["mail"]["host"];
|
||||
$mailer->Port = $this->config["mail"]["port"];
|
||||
$mailer->Username = $this->config["mail"]["username"];
|
||||
$mailer->Password = $this->config["mail"]["password"];
|
||||
$mailer->Host = $config["mail"]["host"];
|
||||
$mailer->Port = $config["mail"]["port"];
|
||||
$mailer->Username = $config["mail"]["username"];
|
||||
$mailer->Password = $config["mail"]["password"];
|
||||
try {
|
||||
$mailer->setFrom($this->config["mail"]["username"], $this->config["mail"]["from_name"]);
|
||||
$mailer->setFrom($config["mail"]["username"], $config["mail"]["from_name"]);
|
||||
} catch (PHPMailerException $exception) {
|
||||
$mailer->smtpClose();
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\tracking;
|
||||
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
|
||||
|
||||
|
@ -29,22 +30,20 @@ class NotifyArticleDeletedEmail extends Email
|
|||
*/
|
||||
public function __construct(string $recipient, string $name)
|
||||
{
|
||||
$this->type = self::TYPE;
|
||||
$this->recipient = $recipient;
|
||||
parent::__construct($this::TYPE . "/" . $name, $recipient);
|
||||
|
||||
$this->name = $name;
|
||||
$this->arg1 = $name;
|
||||
}
|
||||
|
||||
|
||||
public function getSubject(): string
|
||||
public function get_subject(): string
|
||||
{
|
||||
return "$this->name article has been deleted";
|
||||
}
|
||||
|
||||
public function getBody(array $config): string
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = $config["server"]["base_path"];
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
|
||||
return
|
||||
"The Wikipedia article about $this->name has been deleted. " .
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\tracking;
|
||||
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
|
||||
|
||||
|
@ -29,22 +30,20 @@ class NotifyArticleUndeletedEmail extends Email
|
|||
*/
|
||||
public function __construct(string $recipient, string $name)
|
||||
{
|
||||
$this->type = self::TYPE;
|
||||
$this->recipient = $recipient;
|
||||
parent::__construct($this::TYPE . "/" . $name, $recipient);
|
||||
|
||||
$this->name = $name;
|
||||
$this->arg1 = $name;
|
||||
}
|
||||
|
||||
|
||||
public function getSubject(): string
|
||||
public function get_subject(): string
|
||||
{
|
||||
return "$this->name article has been re-created";
|
||||
}
|
||||
|
||||
public function getBody(array $config): string
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = $config["server"]["base_path"];
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
|
||||
return
|
||||
"The Wikipedia article about $this->name has been re-created. " .
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\tracking;
|
||||
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
|
||||
|
||||
|
@ -34,25 +35,21 @@ class NotifyStatusChangedEmail extends Email
|
|||
*/
|
||||
public function __construct(string $recipient, string $name, string $new_status)
|
||||
{
|
||||
$this->type = self::TYPE;
|
||||
$this->recipient = $recipient;
|
||||
parent::__construct($this::TYPE . "/" . $name, $recipient);
|
||||
|
||||
$this->name = $name;
|
||||
$this->arg1 = $name;
|
||||
|
||||
$this->new_status = $new_status;
|
||||
$this->arg2 = $new_status;
|
||||
}
|
||||
|
||||
|
||||
public function getSubject(): string
|
||||
public function get_subject(): string
|
||||
{
|
||||
return "$this->name may be $this->new_status";
|
||||
}
|
||||
|
||||
public function getBody(array $config): string
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = $config["server"]["base_path"];
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
|
||||
return
|
||||
"Someone has edited Wikipedia to state that $this->name is $this->new_status. " .
|
||||
|
|
|
@ -264,8 +264,8 @@ class TrackingManager
|
|||
*
|
||||
* @param array<string, array{"type": ArticleType, "status": PersonStatus|null}> $statuses the current statuses of
|
||||
* people
|
||||
* @return array{"undeletions": string[], "status_changes": array<string, string>} the list of articles that were
|
||||
* actually undeleted, and a mapping of articles that were actually changes to the new status
|
||||
* @return array{string[], array<string, string>} the list of articles that were actually undeleted, and a mapping
|
||||
* of articles that were actually changes to the new status
|
||||
*/
|
||||
public function update_statuses(array $statuses): array
|
||||
{
|
||||
|
@ -274,6 +274,7 @@ class TrackingManager
|
|||
|
||||
Database::transaction($this->conn, function () use ($statuses, &$undeletions, &$status_changes) {
|
||||
// Query to mark person as no longer deleted, returning `name` to determine whether something changed
|
||||
// TODO: Split this into two methods, one for `undelete`, one for `update_statuses`?
|
||||
$undelete = $this->conn->prepare("UPDATE people
|
||||
SET is_deleted=0
|
||||
WHERE name=:name AND is_deleted<>0
|
||||
|
@ -302,6 +303,6 @@ class TrackingManager
|
|||
}
|
||||
});
|
||||
|
||||
return ["undeletions" => $undeletions, "status_changes" => $status_changes];
|
||||
return [$undeletions, $status_changes];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace com\fwdekker\deathnotifier\tracking;
|
|||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionException;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\LoggerUtil;
|
||||
use com\fwdekker\deathnotifier\mailer\MailManager;
|
||||
|
@ -49,15 +50,15 @@ class UpdateTrackingsAction extends Action
|
|||
* @param TrackingManager $tracking_manager the manager through which trackings should be updated
|
||||
* @param MediaWiki $mediawiki the instance to connect to Wikipedia with
|
||||
* @param MailManager $mailer the mailer to send emails with
|
||||
* @param string $password the admin password required to perform this function
|
||||
*/
|
||||
public function __construct(PDO $conn, TrackingManager $tracking_manager, MediaWiki $mediawiki, MailManager $mailer,
|
||||
string $password)
|
||||
public function __construct(PDO $conn, TrackingManager $tracking_manager, MediaWiki $mediawiki, MailManager $mailer)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::CLI,
|
||||
"update-trackings",
|
||||
rule_lists: ["password" => [new IsEqualToRule($password, "Incorrect password.")]],
|
||||
rule_lists: [
|
||||
"password" => [new IsEqualToRule(Config::get()["admin"]["cli_secret"], "Incorrect password.")]
|
||||
],
|
||||
);
|
||||
|
||||
$this->logger = LoggerUtil::with_name($this::class);
|
||||
|
@ -101,8 +102,7 @@ class UpdateTrackingsAction extends Action
|
|||
) {
|
||||
$this->tracking_manager->rename_persons($people_statuses->redirects);
|
||||
$actual_deletions = $this->tracking_manager->delete_persons($people_statuses->missing);
|
||||
["undeletions" => $undeletions, "status_changes" => $status_changes] =
|
||||
$this->tracking_manager->update_statuses($people_statuses->results);
|
||||
[$undeletions, $status_changes] = $this->tracking_manager->update_statuses($people_statuses->results);
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\user;
|
||||
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
|
||||
|
||||
|
@ -29,22 +30,20 @@ class ChangedEmailEmail extends Email
|
|||
*/
|
||||
public function __construct(string $recipient, string $token)
|
||||
{
|
||||
$this->type = self::TYPE;
|
||||
$this->recipient = $recipient;
|
||||
parent::__construct($this::TYPE, $recipient);
|
||||
|
||||
$this->token = $token;
|
||||
$this->arg1 = $token;
|
||||
}
|
||||
|
||||
|
||||
public function getSubject(): string
|
||||
public function get_subject(): string
|
||||
{
|
||||
return "Verify your new email address";
|
||||
}
|
||||
|
||||
public function getBody(array $config): string
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = $config["server"]["base_path"];
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
|
||||
|
||||
return
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\user;
|
||||
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
|
||||
|
||||
|
@ -23,19 +24,18 @@ class ChangedPasswordEmail extends Email
|
|||
*/
|
||||
public function __construct(string $recipient)
|
||||
{
|
||||
$this->type = self::TYPE;
|
||||
$this->recipient = $recipient;
|
||||
parent::__construct($this::TYPE, $recipient);
|
||||
}
|
||||
|
||||
|
||||
public function getSubject(): string
|
||||
public function get_subject(): string
|
||||
{
|
||||
return "Your password has been changed";
|
||||
}
|
||||
|
||||
public function getBody(array $config): string
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = $config["server"]["base_path"];
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
|
||||
return
|
||||
"You changed the password of your Death Notifier account." .
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\user;
|
||||
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
|
||||
|
||||
|
@ -29,21 +30,19 @@ class RegisterEmail extends Email
|
|||
*/
|
||||
public function __construct(string $recipient, string $token)
|
||||
{
|
||||
$this->type = self::TYPE;
|
||||
$this->recipient = $recipient;
|
||||
parent::__construct($this::TYPE, $recipient);
|
||||
|
||||
$this->token = $token;
|
||||
$this->arg1 = $token;
|
||||
}
|
||||
|
||||
public function getSubject(): string
|
||||
public function get_subject(): string
|
||||
{
|
||||
return "Welcome to Death Notifier";
|
||||
}
|
||||
|
||||
public function getBody(array $config): string
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = $config["server"]["base_path"];
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
|
||||
|
||||
return
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\user;
|
||||
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
|
||||
|
||||
|
@ -29,22 +30,20 @@ class ResetPasswordEmail extends Email
|
|||
*/
|
||||
public function __construct(string $recipient, string $token)
|
||||
{
|
||||
$this->type = self::TYPE;
|
||||
$this->recipient = $recipient;
|
||||
parent::__construct($this::TYPE, $recipient);
|
||||
|
||||
$this->token = $token;
|
||||
$this->arg1 = $token;
|
||||
}
|
||||
|
||||
|
||||
public function getSubject(): string
|
||||
public function get_subject(): string
|
||||
{
|
||||
return "Reset your password";
|
||||
}
|
||||
|
||||
public function getBody(array $config): string
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = $config["server"]["base_path"];
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$verify_path =
|
||||
"$base_path?action=reset-password&email=" . rawurlencode($this->recipient) . "&token=$this->token";
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\user;
|
||||
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
|
||||
|
||||
|
@ -29,22 +30,20 @@ class VerifyEmailEmail extends Email
|
|||
*/
|
||||
public function __construct(string $recipient, string $token)
|
||||
{
|
||||
$this->type = self::TYPE;
|
||||
$this->recipient = $recipient;
|
||||
parent::__construct($this::TYPE, $recipient);
|
||||
|
||||
$this->token = $token;
|
||||
$this->arg1 = $token;
|
||||
}
|
||||
|
||||
|
||||
public function getSubject(): string
|
||||
public function get_subject(): string
|
||||
{
|
||||
return "Verify your email address";
|
||||
}
|
||||
|
||||
public function getBody(array $config): string
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = $config["server"]["base_path"];
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
|
||||
|
||||
return
|
||||
|
|
Loading…
Reference in New Issue