From 0cf3897f79c9fdc673c5c4fd06b81a26239c81d2 Mon Sep 17 00:00:00 2001 From: "Florine W. Dekker" Date: Thu, 1 Dec 2022 20:32:12 +0100 Subject: [PATCH] Improve documentation, move classes around, etc. --- composer.json | 2 +- composer.lock | Bin 77196 -> 77196 bytes package-lock.json | Bin 226176 -> 226174 bytes package.json | 4 +- src/main/api.php | 101 ++++++++++-------- .../php/com/fwdekker/deathnotifier/Action.php | 53 ++++++--- .../deathnotifier/ActionDispatcher.php | 56 +++++++--- .../deathnotifier/ActionException.php | 15 ++- .../fwdekker/deathnotifier/ActionMethod.php | 19 +++- .../com/fwdekker/deathnotifier/CliAction.php | 54 ++++++++++ .../deathnotifier/EmulateCronCliAction.php | 64 +++++++++++ .../com/fwdekker/deathnotifier/Mediawiki.php | 1 + .../deathnotifier/StartSessionAction.php | 28 ++++- .../php/com/fwdekker/deathnotifier/Util.php | 27 +++-- .../fwdekker/deathnotifier/cli/CliAction.php | 34 ------ .../deathnotifier/cli/EmulateCronAction.php | 35 ------ .../cli/ProcessEmailQueueAction.php | 27 ----- .../cli/UpdateTrackingsAction.php | 27 ----- .../fwdekker/deathnotifier/mailer/Email.php | 10 ++ .../mailer/ProcessEmailQueueCliAction.php | 44 ++++++++ .../trackings/AddTrackingAction.php | 8 ++ .../NotifyArticleDeletedEmail.php | 4 +- .../NotifyArticleUndeletedEmail.php | 4 +- .../NotifyStatusChangedEmail.php | 4 +- .../trackings/TrackingManager.php | 5 +- .../trackings/UpdateTrackingsCliAction.php | 44 ++++++++ .../deathnotifier/validator/HasLengthRule.php | 2 +- .../deathnotifier/validator/IsEmailRule.php | 2 +- .../deathnotifier/validator/IsEqualToRule.php | 2 +- .../validator/IsNotBlankRule.php | 2 +- .../deathnotifier/validator/IsNotSetRule.php | 2 +- .../deathnotifier/validator/IsSetRule.php | 2 +- .../fwdekker/deathnotifier/validator/Rule.php | 2 +- .../validator/ValidationException.php | 15 ++- .../deathnotifier/DatabaseTestCase.php | 1 + .../deathnotifier/UserManagerTest.php | 11 +- 36 files changed, 481 insertions(+), 230 deletions(-) create mode 100644 src/main/php/com/fwdekker/deathnotifier/CliAction.php create mode 100644 src/main/php/com/fwdekker/deathnotifier/EmulateCronCliAction.php delete mode 100644 src/main/php/com/fwdekker/deathnotifier/cli/CliAction.php delete mode 100644 src/main/php/com/fwdekker/deathnotifier/cli/EmulateCronAction.php delete mode 100644 src/main/php/com/fwdekker/deathnotifier/cli/ProcessEmailQueueAction.php delete mode 100644 src/main/php/com/fwdekker/deathnotifier/cli/UpdateTrackingsAction.php create mode 100644 src/main/php/com/fwdekker/deathnotifier/mailer/ProcessEmailQueueCliAction.php rename src/main/php/com/fwdekker/deathnotifier/{mailer => trackings}/NotifyArticleDeletedEmail.php (94%) rename src/main/php/com/fwdekker/deathnotifier/{mailer => trackings}/NotifyArticleUndeletedEmail.php (94%) rename src/main/php/com/fwdekker/deathnotifier/{mailer => trackings}/NotifyStatusChangedEmail.php (95%) create mode 100644 src/main/php/com/fwdekker/deathnotifier/trackings/UpdateTrackingsCliAction.php diff --git a/composer.json b/composer.json index 44a2c98..85dd4f7 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 03639c09a137af9fcc570d6f82f6622da7bd1f7d..6776f93843f7adcbaf0ee2e23b4666d51169202f 100644 GIT binary patch delta 51 zcmeCV&C+w5WrHxILW-Gzsd=)YaiT%8QEFn6g}JGbiKTg}xq)%Ak%56pQnNARc4J1y GNF4x`mku%j delta 51 zcmeCV&C+w5WrHxIf@zX*YI2f=rKzz&qGgJiX|i#mnWb5hrCF+(Nt&6Nd9yL&c4J1y GNF4y5vJQ>_ diff --git a/package-lock.json b/package-lock.json index eb2bfe9379a08c4cdab56d44f4f16391a1f90664..ca00678e7b4d1965a592a8c9dff3e4a6c2ec7f37 100644 GIT binary patch delta 265 zcmZp8&in5f?*wCJQ$53p=2NCCiZcptyuXie`ll8q(dJD|+cz;W=B}9DG@sFOx{e#O zTUnI56$9_21Bp;_gomZ`Y` zMxl|u9+j49Zow|ek?FyH`5qR<5y@F58Sc)(t{FKlQI?@TsV+r{lP4PbwcD;?+-|#u VY1>S?SUH_>HIvtNUU%kV9RRx(TTK7} delta 278 zcmezOjJM%A?*wBO6FozNiDpw6O{N#BG6`?IyN_|YY6`PR^LnQ3>zNqyR)BZ{(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"] -))); +])); diff --git a/src/main/php/com/fwdekker/deathnotifier/Action.php b/src/main/php/com/fwdekker/deathnotifier/Action.php index b58eace..3fa3177 100644 --- a/src/main/php/com/fwdekker/deathnotifier/Action.php +++ b/src/main/php/com/fwdekker/deathnotifier/Action.php @@ -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 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 $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) diff --git a/src/main/php/com/fwdekker/deathnotifier/ActionDispatcher.php b/src/main/php/com/fwdekker/deathnotifier/ActionDispatcher.php index e073180..ce7c388 100644 --- a/src/main/php/com/fwdekker/deathnotifier/ActionDispatcher.php +++ b/src/main/php/com/fwdekker/deathnotifier/ActionDispatcher.php @@ -2,36 +2,66 @@ namespace com\fwdekker\deathnotifier; +use InvalidArgumentException; + +/** + * Dispatches actions to the right implementation. + */ class ActionDispatcher { /** - * @var array + * @var array> 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); } } diff --git a/src/main/php/com/fwdekker/deathnotifier/ActionException.php b/src/main/php/com/fwdekker/deathnotifier/ActionException.php index fdacd84..eb640d4 100644 --- a/src/main/php/com/fwdekker/deathnotifier/ActionException.php +++ b/src/main/php/com/fwdekker/deathnotifier/ActionException.php @@ -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); diff --git a/src/main/php/com/fwdekker/deathnotifier/ActionMethod.php b/src/main/php/com/fwdekker/deathnotifier/ActionMethod.php index 517fd4e..b185d71 100644 --- a/src/main/php/com/fwdekker/deathnotifier/ActionMethod.php +++ b/src/main/php/com/fwdekker/deathnotifier/ActionMethod.php @@ -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 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, }; diff --git a/src/main/php/com/fwdekker/deathnotifier/CliAction.php b/src/main/php/com/fwdekker/deathnotifier/CliAction.php new file mode 100644 index 0000000..c47d151 --- /dev/null +++ b/src/main/php/com/fwdekker/deathnotifier/CliAction.php @@ -0,0 +1,54 @@ + $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(); + } +} diff --git a/src/main/php/com/fwdekker/deathnotifier/EmulateCronCliAction.php b/src/main/php/com/fwdekker/deathnotifier/EmulateCronCliAction.php new file mode 100644 index 0000000..32546a3 --- /dev/null +++ b/src/main/php/com/fwdekker/deathnotifier/EmulateCronCliAction.php @@ -0,0 +1,64 @@ +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); + } + } +} diff --git a/src/main/php/com/fwdekker/deathnotifier/Mediawiki.php b/src/main/php/com/fwdekker/deathnotifier/Mediawiki.php index 5d20360..e556963 100644 --- a/src/main/php/com/fwdekker/deathnotifier/Mediawiki.php +++ b/src/main/php/com/fwdekker/deathnotifier/Mediawiki.php @@ -114,6 +114,7 @@ class Mediawiki * * @param array $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 the API's response * @throws Exception if the query fails */ diff --git a/src/main/php/com/fwdekker/deathnotifier/StartSessionAction.php b/src/main/php/com/fwdekker/deathnotifier/StartSessionAction.php index 9226909..1708869 100644 --- a/src/main/php/com/fwdekker/deathnotifier/StartSessionAction.php +++ b/src/main/php/com/fwdekker/deathnotifier/StartSessionAction.php @@ -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; } diff --git a/src/main/php/com/fwdekker/deathnotifier/Util.php b/src/main/php/com/fwdekker/deathnotifier/Util.php index 2bd2c10..3b3d79c 100644 --- a/src/main/php/com/fwdekker/deathnotifier/Util.php +++ b/src/main/php/com/fwdekker/deathnotifier/Util.php @@ -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>|null the configuration + * @return array> 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|null the parsed POST-ed values + * @return array 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 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. diff --git a/src/main/php/com/fwdekker/deathnotifier/cli/CliAction.php b/src/main/php/com/fwdekker/deathnotifier/cli/CliAction.php deleted file mode 100644 index ecaf583..0000000 --- a/src/main/php/com/fwdekker/deathnotifier/cli/CliAction.php +++ /dev/null @@ -1,34 +0,0 @@ -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(); - } -} diff --git a/src/main/php/com/fwdekker/deathnotifier/cli/EmulateCronAction.php b/src/main/php/com/fwdekker/deathnotifier/cli/EmulateCronAction.php deleted file mode 100644 index bccc8f5..0000000 --- a/src/main/php/com/fwdekker/deathnotifier/cli/EmulateCronAction.php +++ /dev/null @@ -1,35 +0,0 @@ -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); - } - } -} diff --git a/src/main/php/com/fwdekker/deathnotifier/cli/ProcessEmailQueueAction.php b/src/main/php/com/fwdekker/deathnotifier/cli/ProcessEmailQueueAction.php deleted file mode 100644 index 2095280..0000000 --- a/src/main/php/com/fwdekker/deathnotifier/cli/ProcessEmailQueueAction.php +++ /dev/null @@ -1,27 +0,0 @@ -mailer = $mailer; - } - - - public function handle(): string - { - $this->mailer->process_queue(); - - return "Successfully processed email queue."; - } -} diff --git a/src/main/php/com/fwdekker/deathnotifier/cli/UpdateTrackingsAction.php b/src/main/php/com/fwdekker/deathnotifier/cli/UpdateTrackingsAction.php deleted file mode 100644 index f0cad7d..0000000 --- a/src/main/php/com/fwdekker/deathnotifier/cli/UpdateTrackingsAction.php +++ /dev/null @@ -1,27 +0,0 @@ -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."; - } -} diff --git a/src/main/php/com/fwdekker/deathnotifier/mailer/Email.php b/src/main/php/com/fwdekker/deathnotifier/mailer/Email.php index 167280a..0cce100 100644 --- a/src/main/php/com/fwdekker/deathnotifier/mailer/Email.php +++ b/src/main/php/com/fwdekker/deathnotifier/mailer/Email.php @@ -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), diff --git a/src/main/php/com/fwdekker/deathnotifier/mailer/ProcessEmailQueueCliAction.php b/src/main/php/com/fwdekker/deathnotifier/mailer/ProcessEmailQueueCliAction.php new file mode 100644 index 0000000..d76b39c --- /dev/null +++ b/src/main/php/com/fwdekker/deathnotifier/mailer/ProcessEmailQueueCliAction.php @@ -0,0 +1,44 @@ +mailer = $mailer; + } + + + /** + * Processes the queue. + * + * @return mixed `null` + */ + public function handle(): mixed + { + $this->mailer->process_queue(); + + return null; + } +} diff --git a/src/main/php/com/fwdekker/deathnotifier/trackings/AddTrackingAction.php b/src/main/php/com/fwdekker/deathnotifier/trackings/AddTrackingAction.php index b6bf39a..6dcd50c 100644 --- a/src/main/php/com/fwdekker/deathnotifier/trackings/AddTrackingAction.php +++ b/src/main/php/com/fwdekker/deathnotifier/trackings/AddTrackingAction.php @@ -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( diff --git a/src/main/php/com/fwdekker/deathnotifier/mailer/NotifyArticleDeletedEmail.php b/src/main/php/com/fwdekker/deathnotifier/trackings/NotifyArticleDeletedEmail.php similarity index 94% rename from src/main/php/com/fwdekker/deathnotifier/mailer/NotifyArticleDeletedEmail.php rename to src/main/php/com/fwdekker/deathnotifier/trackings/NotifyArticleDeletedEmail.php index 047ca77..5feb139 100644 --- a/src/main/php/com/fwdekker/deathnotifier/mailer/NotifyArticleDeletedEmail.php +++ b/src/main/php/com/fwdekker/deathnotifier/trackings/NotifyArticleDeletedEmail.php @@ -1,6 +1,8 @@ $statuses the current statuses of + * @param array $statuses the current statuses of * people * @return void */ diff --git a/src/main/php/com/fwdekker/deathnotifier/trackings/UpdateTrackingsCliAction.php b/src/main/php/com/fwdekker/deathnotifier/trackings/UpdateTrackingsCliAction.php new file mode 100644 index 0000000..d37b88b --- /dev/null +++ b/src/main/php/com/fwdekker/deathnotifier/trackings/UpdateTrackingsCliAction.php @@ -0,0 +1,44 @@ +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; + } +} diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/HasLengthRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/HasLengthRule.php index 4b14c82..47d1dbd 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/HasLengthRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/HasLengthRule.php @@ -40,7 +40,7 @@ class HasLengthRule extends Rule /** * Verifies that the input is of the specific length. * - * @param array $inputs the list of inputs in which the value at `key` should be checked + * @param array $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 diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsEmailRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsEmailRule.php index 655a067..377f30d 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsEmailRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/IsEmailRule.php @@ -13,7 +13,7 @@ class IsEmailRule extends Rule /** * Verifies that the input is an email address. * - * @param array $inputs the list of inputs in which the value at `key` should be checked + * @param array $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 diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsEqualToRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsEqualToRule.php index 8007297..b2e141f 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsEqualToRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/IsEqualToRule.php @@ -32,7 +32,7 @@ class IsEqualToRule extends Rule /** * Verifies that the input has the specified value. * - * @param array $inputs the list of inputs in which the value at `key` should be checked + * @param array $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` diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsNotBlankRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsNotBlankRule.php index 1a8c853..94d4045 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsNotBlankRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/IsNotBlankRule.php @@ -13,7 +13,7 @@ class IsNotBlankRule extends Rule /** * Verifies that the input is not blank. * - * @param array $inputs the list of inputs in which the value at `key` should be checked + * @param array $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 diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsNotSetRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsNotSetRule.php index 90e9afa..b7734df 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsNotSetRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/IsNotSetRule.php @@ -13,7 +13,7 @@ class IsNotSetRule extends Rule /** * Verifies that the input is not set. * - * @param array $inputs the list of inputs in which the value at `key` should be checked + * @param array $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 diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsSetRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsSetRule.php index 426039c..5ffb681 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsSetRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/IsSetRule.php @@ -13,7 +13,7 @@ class IsSetRule extends Rule /** * Verifies that the input is set. * - * @param array $inputs the list of inputs in which the value at `key` should be checked + * @param array $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 diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/Rule.php b/src/main/php/com/fwdekker/deathnotifier/validator/Rule.php index cc9fc23..e590adf 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/Rule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/Rule.php @@ -35,7 +35,7 @@ abstract class Rule * * Implementations should never assume that the `$inputs[$key]` is set. * - * @param array $inputs the list of inputs in which the value at `key` should be checked + * @param array $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 diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/ValidationException.php b/src/main/php/com/fwdekker/deathnotifier/validator/ValidationException.php index 7442614..9831080 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/ValidationException.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/ValidationException.php @@ -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); diff --git a/src/test/php/com/fwdekker/deathnotifier/DatabaseTestCase.php b/src/test/php/com/fwdekker/deathnotifier/DatabaseTestCase.php index 925cf3f..4464f86 100644 --- a/src/test/php/com/fwdekker/deathnotifier/DatabaseTestCase.php +++ b/src/test/php/com/fwdekker/deathnotifier/DatabaseTestCase.php @@ -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; diff --git a/src/test/php/com/fwdekker/deathnotifier/UserManagerTest.php b/src/test/php/com/fwdekker/deathnotifier/UserManagerTest.php index a0e28dd..a704946 100644 --- a/src/test/php/com/fwdekker/deathnotifier/UserManagerTest.php +++ b/src/test/php/com/fwdekker/deathnotifier/UserManagerTest.php @@ -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;