Improve documentation, move classes around, etc.

This commit is contained in:
Florine W. Dekker 2022-12-01 20:32:12 +01:00
parent 4fcf615e41
commit 0cf3897f79
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
36 changed files with 481 additions and 230 deletions

View File

@ -1,7 +1,7 @@
{
"name": "fwdekker/death-notifier",
"description": "Get notified when a famous person dies.",
"version": "0.15.0", "_comment_version": "Also update version in `package.json`!",
"version": "0.15.1", "_comment_version": "Also update version in `package.json`!",
"type": "project",
"license": "MIT",
"homepage": "https://git.fwdekker.com/tools/death-notifier",

BIN
composer.lock generated

Binary file not shown.

BIN
package-lock.json generated

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "death-notifier",
"version": "0.15.0", "_comment_version": "Also update version in `composer.json`!",
"version": "0.15.1", "_comment_version": "Also update version in `composer.json`!",
"description": "Get notified when a famous person dies.",
"author": "Florine W. Dekker",
"browser": "dist/bundle.js",
@ -28,7 +28,7 @@
"grunt-run": "^0.8.1",
"grunt-text-replace": "^0.4.0",
"grunt-webpack": "^5.0.0",
"ts-loader": "^9.4.1",
"ts-loader": "^9.4.2",
"typescript": "^4.9.3",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0"

View File

@ -2,11 +2,10 @@
use com\fwdekker\deathnotifier\ActionDispatcher;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\cli\EmulateCronAction;
use com\fwdekker\deathnotifier\cli\ProcessEmailQueueAction;
use com\fwdekker\deathnotifier\cli\UpdateTrackingsAction;
use com\fwdekker\deathnotifier\Database;
use com\fwdekker\deathnotifier\EmulateCronCliAction;
use com\fwdekker\deathnotifier\mailer\Mailer;
use com\fwdekker\deathnotifier\mailer\ProcessEmailQueueCliAction;
use com\fwdekker\deathnotifier\Mediawiki;
use com\fwdekker\deathnotifier\Response;
use com\fwdekker\deathnotifier\StartSessionAction;
@ -14,6 +13,7 @@ use com\fwdekker\deathnotifier\trackings\AddTrackingAction;
use com\fwdekker\deathnotifier\trackings\ListTrackingsAction;
use com\fwdekker\deathnotifier\trackings\RemoveTrackingAction;
use com\fwdekker\deathnotifier\trackings\TrackingManager;
use com\fwdekker\deathnotifier\trackings\UpdateTrackingsCliAction;
use com\fwdekker\deathnotifier\user\GetUserDataAction;
use com\fwdekker\deathnotifier\user\LoginAction;
use com\fwdekker\deathnotifier\user\LogoutAction;
@ -35,7 +35,7 @@ require_once __DIR__ . "/.vendor/autoload.php";
// Preamble
$config = Util::read_config() ?? Util::http_exit(500);
$config = Util::read_config();
// TODO: Improve logging specificity and usefulness
$logger = Util::create_logger($config["logger"]);
$db = new Database($logger->withName("Database"), $config["database"]["filename"]);
@ -45,54 +45,65 @@ $mediawiki = new Mediawiki($logger->withName("Mediawiki"));
$user_manager = new UserManager($logger->withName("UserManager"), $db->conn, $mailer);
$tracking_manager = new TrackingManager($logger->withName("TrackingManager"), $db->conn, $mailer, $mediawiki);
$db->auto_install($mailer, $user_manager, $tracking_manager);
$db->auto_migrate();
session_start();
$_SESSION["token"] = $_SESSION["token"] ?? Util::generate_csrf_token() ?? Util::http_exit(500);
$_POST = Util::parse_post();
// Handle request
try {
session_start();
$_SESSION["token"] = $_SESSION["token"] ?? Util::generate_csrf_token();
$_POST = Util::parse_post();
// Update database
$db->auto_install($mailer, $user_manager, $tracking_manager);
$db->auto_migrate();
// Process request
$dispatcher = new ActionDispatcher();
// GET actions
$dispatcher->register_action(new StartSessionAction($user_manager));
$dispatcher->register_action(new GetUserDataAction($user_manager));
$dispatcher->register_action(new ListTrackingsAction($tracking_manager));
$dispatcher->register_action(new ValidatePasswordResetTokenAction($user_manager));
// POST actions
$dispatcher->register_action(new RegisterAction($user_manager));
$dispatcher->register_action(new LoginAction($user_manager));
$dispatcher->register_action(new LogoutAction());
$dispatcher->register_action(new ResendVerifyEmailAction($user_manager));
$dispatcher->register_action(new VerifyEmailAction($user_manager));
$dispatcher->register_action(new UpdateEmailAction($user_manager));
$dispatcher->register_action(new ToggleNotificationsAction($user_manager));
$dispatcher->register_action(new UpdatePasswordAction($user_manager));
$dispatcher->register_action(new SendPasswordResetAction($user_manager));
$dispatcher->register_action(new ResetPasswordAction($user_manager));
$dispatcher->register_action(new UserDeleteAction($user_manager));
$dispatcher->register_action(new AddTrackingAction($tracking_manager));
$dispatcher->register_action(new RemoveTrackingAction($tracking_manager));
// CLI actions
$dispatcher->register_action(new UpdateTrackingsAction($config, $tracking_manager));
$dispatcher->register_action(new ProcessEmailQueueAction($config, $mailer));
$dispatcher->register_action(new EmulateCronAction($config, $tracking_manager, $mailer));
// Dispatch
if (isset($_GET["action"]))
$response = $dispatcher->handle(ActionMethod::GET, $_GET["action"]);
else if (isset($_POST["action"]))
$response = $dispatcher->handle(ActionMethod::POST, $_POST["action"]);
else if ($argc > 1)
$response = $dispatcher->handle(ActionMethod::CLI, $argv[1]);
else
$response = Response::satisfied();
// Dispatch request
$dispatcher = new ActionDispatcher();
// GET actions
$dispatcher->register_action(new StartSessionAction($user_manager));
$dispatcher->register_action(new GetUserDataAction($user_manager));
$dispatcher->register_action(new ListTrackingsAction($tracking_manager));
$dispatcher->register_action(new ValidatePasswordResetTokenAction($user_manager));
// POST actions
$dispatcher->register_action(new RegisterAction($user_manager));
$dispatcher->register_action(new LoginAction($user_manager));
$dispatcher->register_action(new LogoutAction());
$dispatcher->register_action(new ResendVerifyEmailAction($user_manager));
$dispatcher->register_action(new VerifyEmailAction($user_manager));
$dispatcher->register_action(new UpdateEmailAction($user_manager));
$dispatcher->register_action(new ToggleNotificationsAction($user_manager));
$dispatcher->register_action(new UpdatePasswordAction($user_manager));
$dispatcher->register_action(new SendPasswordResetAction($user_manager));
$dispatcher->register_action(new ResetPasswordAction($user_manager));
$dispatcher->register_action(new UserDeleteAction($user_manager));
$dispatcher->register_action(new AddTrackingAction($tracking_manager));
$dispatcher->register_action(new RemoveTrackingAction($tracking_manager));
// CLI actions
$cli_actions = [
new UpdateTrackingsCliAction($config, $tracking_manager),
new ProcessEmailQueueCliAction($config, $mailer),
];
$dispatcher->register_action($cli_actions[0]);
$dispatcher->register_action($cli_actions[1]);
$dispatcher->register_action(new EmulateCronCliAction($cli_actions));
// Dispatch
if (isset($_GET["action"]))
$response = $dispatcher->handle(ActionMethod::GET);
else if (isset($_POST["action"]))
$response = $dispatcher->handle(ActionMethod::POST);
else if ($argc > 1)
$response = $dispatcher->handle(ActionMethod::CLI);
else
$response = Response::satisfied();
} catch (Exception $exception) {
$response = Response::unsatisfied("An unexpected error occurred. Please try again later.");
$logger->error("An unexpected error occurred. Please try again later.", ["cause" => $exception]);
}
// Respond
header("Content-type:application/json;charset=utf-8");
exit(json_encode(array(
exit(json_encode([
"payload" => $response->payload,
"satisfied" => $response->satisfied,
"token" => $_SESSION["token"]
)));
]));

View File

@ -5,22 +5,56 @@ namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\validator\IsEqualToRule;
use com\fwdekker\deathnotifier\validator\IsNotSetRule;
use com\fwdekker\deathnotifier\validator\IsSetRule;
use com\fwdekker\deathnotifier\validator\Rule;
use InvalidArgumentException;
/**
* An action that can be performed in response to a request from the user.
*/
abstract class Action
{
/**
* @var bool `true` if and only if this action requires the user to be logged in
*/
private readonly bool $require_logged_in;
/**
* @var bool `true` if and only if this action requires the user to be logged out
*/
private readonly bool $require_logged_out;
/**
* @var bool `true` if and only if this action requires the request to have a valid CSRF token
*/
private readonly bool $require_valid_csrf_token;
/**
* @var array<string, Rule[]> maps input keys to {@see Rule}s that should be validated before this action is handled
*/
private readonly array $rule_lists;
/**
* @var ActionMethod the method that this action handles
*/
public readonly ActionMethod $method;
public readonly string $action;
/**
* @var string the name of the action that this action handles
*/
public readonly string $name;
/**
* Constructs a new action.
*
* @param ActionMethod $method the method that this action handles
* @param string $name the name of the action that this action handles
* @param bool $require_logged_in `true` if and only if this action requires the user to be logged in
* @param bool $require_logged_out `true` if and only if this action requires the user to be logged out
* @param bool $require_valid_csrf_token `true` if and only if this action requires the request to have a valid CSRF
* token
* @param array<string, Rule[]> $rule_lists maps input keys to {@see Rule}s that should be validated before this
* action is handled
*/
public function __construct(ActionMethod $method,
string $action,
string $name,
bool $require_logged_in = false,
bool $require_logged_out = false,
bool $require_valid_csrf_token = false,
@ -30,7 +64,7 @@ abstract class Action
throw new InvalidArgumentException("Cannot require that user is both logged in and logged out.");
$this->method = $method;
$this->action = $action;
$this->name = $name;
$this->require_logged_in = $require_logged_in;
$this->require_logged_out = $require_logged_out;
@ -39,13 +73,8 @@ abstract class Action
}
final function can_handle(ActionMethod $method, string $action): bool
{
return $method === $this->method && $action === $this->action;
}
/**
* Validates inputs, throwing an exception if any input is invalid.
* Validates inputs according to `rule_lists`, throwing an exception if any input is invalid.
*
* @return void if the input is valid
* @throws ValidationException if the input is invalid
@ -59,10 +88,8 @@ abstract class Action
if ($this->require_logged_out)
(new IsNotSetRule("You must be logged out to perform this action."))->check($_SESSION, "uuid");
if ($this->require_valid_csrf_token)
(new IsEqualToRule(
$_SESSION["token"],
"Invalid request token. Please refresh the page and try again."
))->check($inputs, "token");
(new IsEqualToRule($_SESSION["token"], "Invalid request token. Please refresh the page and try again."))
->check($inputs, "token");
foreach ($this->rule_lists as $key => $rule_list)
foreach ($rule_list as $rule)

View File

@ -2,36 +2,66 @@
namespace com\fwdekker\deathnotifier;
use InvalidArgumentException;
/**
* Dispatches actions to the right implementation.
*/
class ActionDispatcher
{
/**
* @var array<Action>
* @var array<string, array<string, Action>> the registered actions
*/
private array $actions = array();
private array $actions = [];
/**
* Registers `action` so that `handle` can find it.
*
* Only one action can be registered given a combination of method and action name. An exception is thrown if
* this restriction is violated, because that is likely to be a mistake.
*
* @param Action $action the action to register
* @return void
*/
public function register_action(Action $action): void
{
$this->actions[] = $action;
$method = $action->method->name;
if (!isset($this->actions[$method]))
$this->actions[$method] = [];
if (isset($this->actions[$method][$action->name]))
// TODO: Throw more specific exceptions(?)
throw new InvalidArgumentException("Cannot register another handler for $method action '$action->name'.");
$this->actions[$method][$action->name] = $action;
}
public function handle(ActionMethod $method, string $action_name): Response
/**
* Executes the registered action for the given pair of method and action name.
*
* @param ActionMethod $method the method of the action to execute
* @return Response a satisfied response with the action's output if the action did not throw an exception, or an
* unsatisfied response with the exception's message and target
*/
public function handle(ActionMethod $method): Response
{
$suitable_actions = array_filter($this->actions, fn($action) => $action->can_handle($method, $action_name));
if (empty($suitable_actions))
return Response::unsatisfied("Unknown $method->name action '$action_name'.");
if (sizeof($suitable_actions) > 1)
return Response::unsatisfied("Multiple handlers for $method->name action '$action_name'.");
$inputs = $method->get_inputs();
if (!isset($inputs["action"]))
throw new InvalidArgumentException("No action specified.");
$action = $suitable_actions[array_key_first($suitable_actions)];
$action_name = $inputs["action"];
if (!isset($this->actions[$method->name]) || !isset($this->actions[$method->name][$action_name]))
throw new InvalidArgumentException("No handler for $method->name action '$action_name'.");
$action = $this->actions[$method->name][$action_name];
try {
$action->validate_inputs();
$payload = $action->handle();
return Response::satisfied($payload);
} catch (ValidationException $exception) {
return Response::unsatisfied($exception->getMessage(), $exception->target);
} catch (ActionException $exception) {
} catch (ActionException|ValidationException $exception) {
return Response::unsatisfied($exception->getMessage(), $exception->target);
}
}

View File

@ -5,12 +5,25 @@ namespace com\fwdekker\deathnotifier;
use Exception;
/**
* Thrown if an action could not be completed.
*/
class ActionException extends Exception
{
/**
* @var string|null the input element that caused the exception, or `null` if no such element could be identified
*/
public readonly ?string $target;
public function __construct(?string $message = null, ?string $target = null)
/**
* Constructs a new `ActionException`.
*
* @param string $message the message to show to the user
* @param string|null $target the input element that caused the exception, or `null` if no such element could be
* identified
*/
public function __construct(string $message, ?string $target = null)
{
parent::__construct($message);

View File

@ -3,17 +3,34 @@
namespace com\fwdekker\deathnotifier;
/**
* The method by which a user requests an action.
*/
enum ActionMethod
{
/**
* Command-line interface.
*/
case CLI;
/**
* HTTP GET request.
*/
case GET;
/**
* HTTP POST request.
*/
case POST;
/**
* Returns the user's inputs corresponding to this request method.
*
* @return array<int|string, mixed> the user's inputs corresponding to this request method
*/
function get_inputs(): array
{
return match ($this) {
ActionMethod::CLI => $_SERVER["argv"],
ActionMethod::CLI => Util::parse_cli(),
ActionMethod::GET => $_GET,
ActionMethod::POST => $_POST,
};

View File

@ -0,0 +1,54 @@
<?php
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\validator\Rule;
/**
* An action for the CLI, which requires a valid password to be executed.
*/
abstract class CliAction extends Action
{
/**
* @var mixed the application's configuration
*/
private mixed $config;
/**
* Constructs a new CLI action.
*
* @param mixed $config the application's configuration
* @param string $name the name of the action that this action handles
* @param array<string, Rule[]> $rule_lists maps input keys to {@see Rule}s that should be validated before this
* action is handled
*/
public function __construct(mixed $config, string $name, array $rule_lists = [])
{
parent::__construct(ActionMethod::CLI, $name, rule_lists: $rule_lists);
$this->config = $config;
}
/**
* Validates the admin password, and then validates remaining inputs as with any {@see Action}.
*
* @return void
* @throws ValidationException if the input is invalid
*/
public function validate_inputs(): void
{
$inputs = $this->method->get_inputs();
if (hash_equals($this->config["admin"]["cli_secret"], "REPLACE THIS WITH A SECRET VALUE"))
throw new ValidationException("Default config value for 'cli_secret' detected. CLI access disabled.");
if (!isset($inputs["password"]))
throw new ValidationException("Password required. Specify a password using `password=...`.");
// TODO: Read input password from file specified as an input argument in `$argv` (= `$inputs`)
if (!hash_equals($this->config["admin"]["cli_secret"], $inputs["password"]))
throw new ValidationException("Incorrect password.");
parent::validate_inputs();
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace com\fwdekker\deathnotifier;
/**
* Periodically executes several other actions, as if cron jobs have been set up to do so.
*
* Intended for local development only.
*/
class EmulateCronCliAction extends Action
{
/**
* The number of seconds between executing tasks.
*/
private const INTERVAL = 15;
/**
* @var Action[] the actions to execute at an interval
*/
private readonly array $actions;
/**
* Constructs a new `EmulateCronAction`.
*
* @param Action[] $actions the actions to execute at an interval
*/
public function __construct(array $actions)
{
parent::__construct(ActionMethod::CLI, "emulate-cron");
$this->actions = $actions;
}
/**
* Validates the inputs of each registered action.
*
* @return void if the input is valid
* @throws ValidationException if the input is invalid
*/
public function validate_inputs(): void
{
foreach ($this->actions as $action)
$action->validate_inputs();
}
/**
* Updates all trackings and processes the mail queue at a regular interval.
*
* @return never
*/
public function handle(): never
{
while (true) {
print("Emulating cron jobs.\n");
foreach ($this->actions as $action)
$action->handle();
print("Done\n");
sleep(self::INTERVAL);
}
}
}

View File

@ -114,6 +114,7 @@ class Mediawiki
*
* @param array<string, string> $params the parameters to include in each query
* @param string[] $titles the titles to query
* @param string|null $continue_name the name of the continue parameter used for this request by the API
* @return QueryOutput<mixed> the API's response
* @throws Exception if the query fails
*/

View File

@ -6,11 +6,22 @@ use com\fwdekker\deathnotifier\user\UserManager;
use Exception;
/**
* Starts a new user session, or continues an existing one.
*/
class StartSessionAction extends Action
{
/**
* @var UserManager the manager to validate the session through
*/
private readonly UserManager $user_manager;
/**
* Constructs a new `StartSessionAction`.
*
* @param UserManager $user_manager the manager to validate the session through
*/
public function __construct(UserManager $user_manager)
{
parent::__construct(ActionMethod::GET, "start-session");
@ -19,6 +30,13 @@ class StartSessionAction extends Action
}
/**
* Starts a new user session, or continues an existing one.
*
* @return array{"logged_in": bool, "global_message"?: string} whether the user is logged in, and the message to be
* displayed at the top of the page, if any
* @throws ActionException if no CSRF token could be generated
*/
function handle(): array
{
$payload = [];
@ -26,7 +44,9 @@ class StartSessionAction extends Action
// Check if user is logged in
if (!isset($_SESSION["uuid"])) {
$payload["logged_in"] = false;
} else if (!$this->user_manager->user_exists($_SESSION["uuid"])) {
} else if ($this->user_manager->user_exists($_SESSION["uuid"])) {
$payload["logged_in"] = true;
} else {
// User account was deleted
session_destroy();
session_start();
@ -37,13 +57,11 @@ class StartSessionAction extends Action
}
$payload["logged_in"] = false;
} else {
$payload["logged_in"] = true;
}
// Read global message
if (isset($config["server"]["global_message"]) && trim($config["server"]["global_message"]) !== "")
$payload["global_message"] = trim($config["server"]["global_message"]);
if (isset($this->config["server"]["global_message"]) && trim($this->config["server"]["global_message"]) !== "")
$payload["global_message"] = trim($this->config["server"]["global_message"]);
return $payload;
}

View File

@ -3,6 +3,7 @@
namespace com\fwdekker\deathnotifier;
use Exception;
use InvalidArgumentException;
use Monolog\ErrorHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
@ -29,17 +30,17 @@ class Util
/**
* Reads the configuration file and overrides it with the user's custom values.
*
* @return array<string, array<string, mixed>>|null the configuration
* @return array<string, array<string, mixed>> the configuration
*/
static function read_config(): ?array
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) return null;
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) return null;
if ($config_custom === false) throw new InvalidArgumentException("Invalid `config.ini.php` file.");
$config = array_replace_recursive($config, $config_custom);
}
@ -65,9 +66,9 @@ class Util
/**
* Parses POST values from JSON-based inputs.
*
* @return array<string, mixed>|null the parsed POST-ed values
* @return array<int|string, mixed> the parsed POST-ed values
*/
static function parse_post(): ?array
static function parse_post(): array
{
$output = $_POST;
@ -78,6 +79,20 @@ class Util
return $output;
}
/**
* Parses `$argv` into an array, similar to `$_GET`.
*
* Code from https://www.php.net/manual/en/features.commandline.php#108883.
*
* @return array<int|string, mixed> the parsed CLI inputs
*/
static function parse_cli(): array
{
parse_str(implode("&", array_slice($_SERVER["argv"], 1)), $output);
return $output;
}
/**
* Generates an appropriate CSRF token.

View File

@ -1,34 +0,0 @@
<?php
namespace com\fwdekker\deathnotifier\cli;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\ValidationException;
abstract class CliAction extends Action
{
private mixed $config;
public function __construct(mixed $config, string $action, array $rule_lists = [])
{
parent::__construct(ActionMethod::CLI, $action, rule_lists: $rule_lists);
$this->config = $config;
}
public function validate_inputs(): void
{
$inputs = $this->method->get_inputs();
// TODO: Read secret from file specified from `inputs`
if (hash_equals($this->config["admin"]["cli_secret"], "REPLACE THIS WITH A SECRET VALUE"))
throw new ValidationException("Default value for 'cli_secret' detected. Feature disabled.");
if (!hash_equals($this->config["admin"]["cli_secret"], $inputs[2]))
throw new ValidationException("Incorrect value for 'cli_secret'.");
parent::validate_inputs();
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace com\fwdekker\deathnotifier\cli;
use com\fwdekker\deathnotifier\mailer\Mailer;
use com\fwdekker\deathnotifier\trackings\TrackingManager;
class EmulateCronAction extends CliAction
{
private readonly TrackingManager $tracking_manager;
private readonly Mailer $mailer;
public function __construct(mixed $config, TrackingManager $tracking_manager, Mailer $mailer)
{
parent::__construct($config, "update-trackings");
$this->tracking_manager = $tracking_manager;
$this->mailer = $mailer;
}
public function handle(): string
{
while (true) {
print("Updating all trackings\n");
$this->tracking_manager->update_trackings($this->tracking_manager->list_all_unique_person_names());
print("Processing email queue\n");
$this->mailer->process_queue();
print("Done\n");
sleep(15);
}
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace com\fwdekker\deathnotifier\cli;
use com\fwdekker\deathnotifier\mailer\Mailer;
class ProcessEmailQueueAction extends CliAction
{
private readonly Mailer $mailer;
public function __construct(mixed $config, Mailer $mailer)
{
parent::__construct($config, "process-email-queue");
$this->mailer = $mailer;
}
public function handle(): string
{
$this->mailer->process_queue();
return "Successfully processed email queue.";
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace com\fwdekker\deathnotifier\cli;
use com\fwdekker\deathnotifier\trackings\TrackingManager;
class UpdateTrackingsAction extends CliAction
{
private readonly TrackingManager $tracking_manager;
public function __construct(mixed $config, TrackingManager $tracking_manager)
{
parent::__construct($config, "update-trackings");
$this->tracking_manager = $tracking_manager;
}
public function handle(): string
{
$this->tracking_manager->update_trackings($this->tracking_manager->list_all_unique_person_names());
return "Successfully updated all trackings.";
}
}

View File

@ -2,6 +2,14 @@
namespace com\fwdekker\deathnotifier\mailer;
use com\fwdekker\deathnotifier\trackings\NotifyArticleDeletedEmail;
use com\fwdekker\deathnotifier\trackings\NotifyArticleUndeletedEmail;
use com\fwdekker\deathnotifier\trackings\NotifyStatusChangedEmail;
use com\fwdekker\deathnotifier\user\ChangedEmailEmail;
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 Exception;
@ -61,6 +69,8 @@ abstract class 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),

View File

@ -0,0 +1,44 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
use com\fwdekker\deathnotifier\CliAction;
/**
* Processes the queue of emails to send.
*/
class ProcessEmailQueueCliAction extends CliAction
{
/**
* @var Mailer the mailer to process the queue with
*/
private readonly Mailer $mailer;
/**
* Constructs a new `ProcessEmailQueueAction`.
*
* @param mixed $config the application's configuration
* @param Mailer $mailer the mailer to process the queue with
*/
public function __construct(mixed $config, Mailer $mailer)
{
parent::__construct($config, "process-email-queue");
$this->mailer = $mailer;
}
/**
* Processes the queue.
*
* @return mixed `null`
*/
public function handle(): mixed
{
$this->mailer->process_queue();
return null;
}
}

View File

@ -9,11 +9,19 @@ use com\fwdekker\deathnotifier\validator\HasLengthRule;
use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
/**
* Adds a tracking.
*/
class AddTrackingAction extends Action
{
private readonly TrackingManager $tracking_manager;
/**
* Constructs a new `AddTrackingAction`.
*
* @param TrackingManager $tracking_manager the manager to add the tracking to
*/
public function __construct(TrackingManager $tracking_manager)
{
parent::__construct(

View File

@ -1,6 +1,8 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
namespace com\fwdekker\deathnotifier\trackings;
use com\fwdekker\deathnotifier\mailer\Email;
/**

View File

@ -1,6 +1,8 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
namespace com\fwdekker\deathnotifier\trackings;
use com\fwdekker\deathnotifier\mailer\Email;
/**

View File

@ -1,6 +1,8 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
namespace com\fwdekker\deathnotifier\trackings;
use com\fwdekker\deathnotifier\mailer\Email;
/**

View File

@ -5,9 +5,6 @@ namespace com\fwdekker\deathnotifier\trackings;
use com\fwdekker\deathnotifier\ArticleType;
use com\fwdekker\deathnotifier\Database;
use com\fwdekker\deathnotifier\mailer\Mailer;
use com\fwdekker\deathnotifier\mailer\NotifyArticleDeletedEmail;
use com\fwdekker\deathnotifier\mailer\NotifyArticleUndeletedEmail;
use com\fwdekker\deathnotifier\mailer\NotifyStatusChangedEmail;
use com\fwdekker\deathnotifier\Mediawiki;
use com\fwdekker\deathnotifier\Response;
use Exception;
@ -344,7 +341,7 @@ class TrackingManager
/**
* Updates peoples' statuses.
*
* @param array<string, array{"status": \com\fwdekker\deathnotifier\trackings\PersonStatus|null, "type": ArticleType}> $statuses the current statuses of
* @param array<string, array{"status": PersonStatus|null, "type": ArticleType}> $statuses the current statuses of
* people
* @return void
*/

View File

@ -0,0 +1,44 @@
<?php
namespace com\fwdekker\deathnotifier\trackings;
use com\fwdekker\deathnotifier\CliAction;
/**
* Updates all trackings that users have added.
*/
class UpdateTrackingsCliAction extends CliAction
{
/**
* @var TrackingManager the manager through which trackings should be updated
*/
private readonly TrackingManager $tracking_manager;
/**
* Constructs a new `UpdateTrackingsAction`.
*
* @param mixed $config the application's configuration
* @param TrackingManager $tracking_manager the manager through which trackings should be updated
*/
public function __construct(mixed $config, TrackingManager $tracking_manager)
{
parent::__construct($config, "update-trackings");
$this->tracking_manager = $tracking_manager;
}
/**
* Updates all trackings that users have added.
*
* @return mixed `null`
*/
public function handle(): mixed
{
$this->tracking_manager->update_trackings($this->tracking_manager->list_all_unique_person_names());
return null;
}
}

View File

@ -40,7 +40,7 @@ class HasLengthRule extends Rule
/**
* Verifies that the input is of the specific length.
*
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param array<int|string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param string $key the key in `inputs` of the input to check
* @return void if `$inputs[$key]` is of the specified length
* @throws ValidationException if `$inputs[$key]` is not set or is not of the specified length

View File

@ -13,7 +13,7 @@ class IsEmailRule extends Rule
/**
* Verifies that the input is an email address.
*
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param array<int|string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param string $key the key in `inputs` of the input to check
* @return void if `$inputs[$key]` is an email address
* @throws ValidationException if `$inputs[$key]` is not set or is not an email address

View File

@ -32,7 +32,7 @@ class IsEqualToRule extends Rule
/**
* Verifies that the input has the specified value.
*
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param array<int|string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param string $key the key in `inputs` of the input to check
* @return void if `$inputs[$key]` equals `$expected`
* @throws ValidationException if `$inputs[$key]` is not set or does not equal `$expected`

View File

@ -13,7 +13,7 @@ class IsNotBlankRule extends Rule
/**
* Verifies that the input is not blank.
*
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param array<int|string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param string $key the key in `inputs` of the input to check
* @return void if `trim($inputs[$key])` is not an empty string
* @throws ValidationException if `$inputs[$key]` is not set or if `trim($inputs[$key])` is an empty string

View File

@ -13,7 +13,7 @@ class IsNotSetRule extends Rule
/**
* Verifies that the input is not set.
*
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param array<int|string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param string $key the key in `inputs` of the input to check
* @return void if `$inputs[$key]` is not set
* @throws ValidationException if `$inputs[$key]` is set

View File

@ -13,7 +13,7 @@ class IsSetRule extends Rule
/**
* Verifies that the input is set.
*
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param array<int|string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param string $key the key in `inputs` of the input to check
* @return void if `$inputs[$key]` is set
* @throws ValidationException if `$inputs[$key]` is not set

View File

@ -35,7 +35,7 @@ abstract class Rule
*
* Implementations should never assume that the `$inputs[$key]` is set.
*
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param array<int|string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param string $key the key in `inputs` of the input to check
* @return void if the rule holds
* @throws ValidationException if the rule does not hold

View File

@ -5,12 +5,25 @@ namespace com\fwdekker\deathnotifier;
use Exception;
/**
* Thrown if an action could not be handled because an input is invalid.
*/
class ValidationException extends Exception
{
/**
* @var string|null the input element that caused the exception, or `null` if no such element could be identified
*/
public readonly ?string $target;
public function __construct(?string $message = null, ?string $target = null)
/**
* Constructs a new `ValidationException`.
*
* @param string $message the message to show to the user
* @param string|null $target the input element that caused the exception, or `null` if no such element could be
* identified
*/
public function __construct(string $message, ?string $target = null)
{
parent::__construct($message);

View File

@ -4,6 +4,7 @@ namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\mailer\Mailer;
use com\fwdekker\deathnotifier\trackings\TrackingManager;
use com\fwdekker\deathnotifier\user\UserManager;
use Exception;
use Monolog\Logger;
use Monolog\Test\TestCase;

View File

@ -2,12 +2,13 @@
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\mailer\ChangedEmailEmail;
use com\fwdekker\deathnotifier\mailer\ChangedPasswordEmail;
use com\fwdekker\deathnotifier\mailer\Mailer;
use com\fwdekker\deathnotifier\mailer\RegisterEmail;
use com\fwdekker\deathnotifier\mailer\ResetPasswordEmail;
use com\fwdekker\deathnotifier\mailer\VerifyEmailEmail;
use com\fwdekker\deathnotifier\user\ChangedEmailEmail;
use com\fwdekker\deathnotifier\user\ChangedPasswordEmail;
use com\fwdekker\deathnotifier\user\RegisterEmail;
use com\fwdekker\deathnotifier\user\ResetPasswordEmail;
use com\fwdekker\deathnotifier\user\UserManager;
use com\fwdekker\deathnotifier\user\VerifyEmailEmail;
use PDO;
use PHPUnit\Framework\MockObject\MockObject;