Create action dispatcher mechanism for cleaner api.php

This commit is contained in:
Florine W. Dekker 2022-12-01 18:24:08 +01:00
parent 1c62c73055
commit 4fcf615e41
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
46 changed files with 1193 additions and 390 deletions

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "death-notifier",
"version": "0.14.11", "_comment_version": "Also update version in `composer.json`!",
"version": "0.15.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",

View File

@ -1,24 +1,40 @@
<?php
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\mailer\Mailer;
use com\fwdekker\deathnotifier\Mediawiki;
use com\fwdekker\deathnotifier\Response;
use com\fwdekker\deathnotifier\StartSessionAction;
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\UserManager;
use com\fwdekker\deathnotifier\user\GetUserDataAction;
use com\fwdekker\deathnotifier\user\LoginAction;
use com\fwdekker\deathnotifier\user\LogoutAction;
use com\fwdekker\deathnotifier\user\RegisterAction;
use com\fwdekker\deathnotifier\user\ResendVerifyEmailAction;
use com\fwdekker\deathnotifier\user\ResetPasswordAction;
use com\fwdekker\deathnotifier\user\SendPasswordResetAction;
use com\fwdekker\deathnotifier\user\ToggleNotificationsAction;
use com\fwdekker\deathnotifier\user\UpdateEmailAction;
use com\fwdekker\deathnotifier\user\UpdatePasswordAction;
use com\fwdekker\deathnotifier\user\UserDeleteAction;
use com\fwdekker\deathnotifier\user\UserManager;
use com\fwdekker\deathnotifier\user\ValidatePasswordResetTokenAction;
use com\fwdekker\deathnotifier\user\VerifyEmailAction;
use com\fwdekker\deathnotifier\Util;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
use com\fwdekker\deathnotifier\validator\IsSetRule;
use com\fwdekker\deathnotifier\validator\LengthRule;
use com\fwdekker\deathnotifier\validator\Validator;
/** @noinspection PhpIncludeInspection Exists after `npm run deploy` */
require_once __DIR__ . "/.vendor/autoload.php";
// Preamble
$_POST = Util::parse_post();
$config = Util::read_config() ?? Util::http_exit(500);
// TODO: Improve logging specificity and usefulness
$logger = Util::create_logger($config["logger"]);
@ -33,238 +49,44 @@ $db->auto_install($mailer, $user_manager, $tracking_manager);
$db->auto_migrate();
session_start();
$_SESSION["token"] = $_SESSION["token"] ?? Util::generate_csrf_token($logger) ?? Util::http_exit(500);
$_SESSION["token"] = $_SESSION["token"] ?? Util::generate_csrf_token() ?? Util::http_exit(500);
$_POST = Util::parse_post();
// Process request
$response = null;
if (isset($_POST["action"])) {
// POST requests; alter state
switch ($_POST["action"]) {
case "register":
$response =
Validator::validate_logged_out($_SESSION) ??
Validator::validate_token($_POST, $_SESSION["token"]) ??
Validator::validate_inputs($_POST,
[
"email" => [new IsEmailRule()],
"password" => [
new LengthRule(UserManager::MIN_PASSWORD_LENGTH, UserManager::MAX_PASSWORD_LENGTH)
]
]
) ??
$user_manager->register_user($_POST["email"], $_POST["password"]);
break;
case "login":
$response =
Validator::validate_logged_out($_SESSION) ??
Validator::validate_token($_POST, $_SESSION["token"]) ??
Validator::validate_inputs($_POST, ["email" => [new IsEmailRule()]]);
if ($response !== null) break;
[$response, $uuid] = $user_manager->check_login($_POST["email"], $_POST["password"]);
if ($response->satisfied) $_SESSION["uuid"] = $uuid;
break;
case "logout":
$response =
Validator::validate_logged_in($_SESSION) ??
Validator::validate_token($_POST, $_SESSION["token"]);
if ($response !== null) break;
session_destroy();
session_start();
$_SESSION["token"] = Util::generate_csrf_token($logger) ?? Util::http_exit(500);
$response = Response::satisfied();
break;
case "update-email":
$response =
Validator::validate_logged_in($_SESSION) ??
Validator::validate_token($_POST, $_SESSION["token"]) ??
Validator::validate_inputs($_POST, ["email" => [new IsEmailRule()]]) ??
$user_manager->set_email($_SESSION["uuid"], $_POST["email"]);
break;
case "verify-email":
$response =
// User does not need to be logged in
Validator::validate_token($_POST, $_SESSION["token"]) ??
Validator::validate_inputs($_POST,
[
"email" => [new IsEmailRule()],
"verify_token" => [new IsSetRule()],
]
) ??
$user_manager->verify_email($_POST["email"], $_POST["verify_token"]);
break;
case "resend-verify-email":
$response =
Validator::validate_logged_in($_SESSION) ??
Validator::validate_token($_POST, $_SESSION["token"]) ??
$user_manager->resend_verify_email($_SESSION["uuid"]);
break;
case "toggle-notifications":
$response =
Validator::validate_logged_in($_SESSION) ??
Validator::validate_token($_POST, $_SESSION["token"]) ??
$user_manager->toggle_notifications($_SESSION["uuid"]);
break;
case "send-password-reset":
$response =
// User does not need to be logged in
Validator::validate_token($_POST, $_SESSION["token"]) ??
Validator::validate_inputs($_POST, ["email" => [new IsEmailRule()]]) ??
$user_manager->send_password_reset($_POST["email"]);
break;
case "reset-password":
$response =
// User does not need to be logged in
Validator::validate_token($_POST, $_SESSION["token"]) ??
Validator::validate_inputs($_POST,
[
"password" => [
new LengthRule(UserManager::MIN_PASSWORD_LENGTH, UserManager::MAX_PASSWORD_LENGTH)
],
]
) ??
$user_manager->reset_password($_POST["email"], $_POST["reset_token"], $_POST["password"]);
break;
case "update-password":
$response =
Validator::validate_logged_in($_SESSION) ??
Validator::validate_token($_POST, $_SESSION["token"]) ??
Validator::validate_inputs($_POST,
[
"password_old" => [new IsSetRule()],
"password_new" => [
new LengthRule(UserManager::MIN_PASSWORD_LENGTH, UserManager::MAX_PASSWORD_LENGTH)
],
]
) ??
$user_manager->set_password($_SESSION["uuid"], $_POST["password_old"], $_POST["password_new"]);
break;
case "user-delete":
$response =
Validator::validate_logged_in($_SESSION) ??
Validator::validate_token($_POST, $_SESSION["token"]) ??
$user_manager->delete_user($_SESSION["uuid"]);
session_destroy();
session_start();
$_SESSION["token"] = Util::generate_csrf_token($logger) ?? Util::http_exit(500);
break;
case "add-tracking":
$response =
Validator::validate_logged_in($_SESSION) ??
Validator::validate_token($_POST, $_SESSION["token"]) ??
Validator::validate_inputs($_POST,
[
"person_name" => [
new LengthRule(TrackingManager::MIN_TITLE_LENGTH, TrackingManager::MAX_TITLE_LENGTH),
new IsNotBlankRule()
],
]
) ??
$tracking_manager->add_tracking($_SESSION["uuid"], $_POST["person_name"]);
break;
case "remove-tracking":
$response =
Validator::validate_logged_in($_SESSION) ??
Validator::validate_token($_POST, $_SESSION["token"]) ??
Validator::validate_inputs($_POST,
[
"person_name" => [
new LengthRule(TrackingManager::MIN_TITLE_LENGTH, TrackingManager::MAX_TITLE_LENGTH),
new IsNotBlankRule()
],
]) ??
$tracking_manager->remove_tracking($_SESSION["uuid"], $_POST["person_name"]);
break;
default:
$response = Response::unsatisfied("Unknown POST action '" . htmlentities($_POST["action"]) . "'.");
break;
}
} elseif (isset($_GET["action"])) {
// GET requests; do not alter state
switch ($_GET["action"]) {
case "start-session":
if (!isset($_SESSION["uuid"])) {
$response = Response::satisfied(["logged_in" => false]);
} else if (!$user_manager->user_exists($_SESSION["uuid"])) {
// User account was deleted
session_destroy();
session_start();
$_SESSION["token"] = Util::generate_csrf_token($logger) ?? Util::http_exit(500);
$response = Response::satisfied(["logged_in" => false]);
} else {
$response = Response::satisfied(["logged_in" => true]);
}
if (isset($config["server"]["global_message"]) && trim($config["server"]["global_message"]) !== "")
$response->payload["global_message"] = trim($config["server"]["global_message"]);
break;
case "validate-password-reset-token":
$response =
// User does not need to be logged in
Validator::validate_token($_GET, $_SESSION["token"]) ??
Validator::validate_inputs($_GET,
[
"reset_token" => [new IsSetRule()],
"email" => [new IsEmailRule()]
]
) ??
$user_manager->validate_password_reset_token($_GET["email"], $_GET["reset_token"]);
break;
case "get-user-data":
$response =
Validator::validate_logged_in($_SESSION) ??
Validator::validate_token($_GET, $_SESSION["token"]) ??
$user_manager->get_user($_SESSION["uuid"]);
break;
case "list-trackings":
$response =
Validator::validate_logged_in($_SESSION) ??
Validator::validate_token($_GET, $_SESSION["token"]) ??
$tracking_manager->list_trackings($_SESSION["uuid"]);
break;
default:
$response = Response::unsatisfied("Unknown GET action '" . htmlentities($_GET["action"]) . "'.");
}
} elseif ($argc > 1) {
// CLI
// TODO: Read secret from file
if (hash_equals($config["admin"]["cli_secret"], "REPLACE THIS WITH A SECRET VALUE"))
exit("Default value for 'cli_secret' detected. Feature disabled.");
if (!hash_equals($config["admin"]["cli_secret"], $argv[2]))
exit("Incorrect value for 'cli_secret'.");
switch ($argv[1]) {
case "emulate-cron":
/* @phpstan-ignore-next-line Intentional infinite loop */
while (true) {
print("Updating all trackings\n");
$tracking_manager->update_trackings($tracking_manager->list_all_unique_person_names());
print("Processing email queue\n");
$mailer->process_queue();
print("Done\n");
sleep(15);
}
case "update-all-trackings":
$logger->info("Updating all trackings.");
$tracking_manager->update_trackings($tracking_manager->list_all_unique_person_names());
exit("Successfully updated all trackings.");
case "process-email-queue":
$logger->info("Processing email queue.");
$mailer->process_queue();
exit("Successfully processed email queue.");
default:
exit("Unknown CLI action '$argv[1]'.");
}
} else {
// No request, no actions, so that's a success
$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();
}
// Respond

View File

@ -0,0 +1,80 @@
<?php
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\validator\IsEqualToRule;
use com\fwdekker\deathnotifier\validator\IsNotSetRule;
use com\fwdekker\deathnotifier\validator\IsSetRule;
use InvalidArgumentException;
abstract class Action
{
private readonly bool $require_logged_in;
private readonly bool $require_logged_out;
private readonly bool $require_valid_csrf_token;
private readonly array $rule_lists;
public readonly ActionMethod $method;
public readonly string $action;
public function __construct(ActionMethod $method,
string $action,
bool $require_logged_in = false,
bool $require_logged_out = false,
bool $require_valid_csrf_token = false,
array $rule_lists = [])
{
if ($require_logged_in && $require_logged_out)
throw new InvalidArgumentException("Cannot require that user is both logged in and logged out.");
$this->method = $method;
$this->action = $action;
$this->require_logged_in = $require_logged_in;
$this->require_logged_out = $require_logged_out;
$this->require_valid_csrf_token = $require_valid_csrf_token;
$this->rule_lists = $rule_lists;
}
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.
*
* @return void if the input is valid
* @throws ValidationException if the input is invalid
*/
function validate_inputs(): void
{
$inputs = $this->method->get_inputs();
if ($this->require_logged_in)
(new IsSetRule("You must be logged in to perform this action."))->check($_SESSION, "uuid");
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");
foreach ($this->rule_lists as $key => $rule_list)
foreach ($rule_list as $rule)
$rule->check($inputs, $key);
}
/**
* Performs the action.
*
* @return mixed the data requested by the action; may be `null`
* @throws ActionException if the action could not be performed
* @throws ValidationException if the inputs are invalid upon further inspection
*/
abstract function handle(): mixed;
}

View File

@ -0,0 +1,38 @@
<?php
namespace com\fwdekker\deathnotifier;
class ActionDispatcher
{
/**
* @var array<Action>
*/
private array $actions = array();
public function register_action(Action $action): void
{
$this->actions[] = $action;
}
public function handle(ActionMethod $method, string $action_name): 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'.");
$action = $suitable_actions[array_key_first($suitable_actions)];
try {
$action->validate_inputs();
$payload = $action->handle();
return Response::satisfied($payload);
} catch (ValidationException $exception) {
return Response::unsatisfied($exception->getMessage(), $exception->target);
} catch (ActionException $exception) {
return Response::unsatisfied($exception->getMessage(), $exception->target);
}
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace com\fwdekker\deathnotifier;
use Exception;
class ActionException extends Exception
{
public readonly ?string $target;
public function __construct(?string $message = null, ?string $target = null)
{
parent::__construct($message);
$this->target = $target;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace com\fwdekker\deathnotifier;
enum ActionMethod
{
case CLI;
case GET;
case POST;
function get_inputs(): array
{
return match ($this) {
ActionMethod::CLI => $_SERVER["argv"],
ActionMethod::GET => $_GET,
ActionMethod::POST => $_POST,
};
}
}

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 Composer\Semver\Comparator;
use Monolog\Logger;
use PDO;

View File

@ -0,0 +1,50 @@
<?php
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\user\UserManager;
use Exception;
class StartSessionAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(ActionMethod::GET, "start-session");
$this->user_manager = $user_manager;
}
function handle(): array
{
$payload = [];
// Check if user is logged in
if (!isset($_SESSION["uuid"])) {
$payload["logged_in"] = false;
} else if (!$this->user_manager->user_exists($_SESSION["uuid"])) {
// User account was deleted
session_destroy();
session_start();
try {
$_SESSION["token"] = Util::generate_csrf_token();
} catch (Exception) {
throw new ActionException("Failed to generate new CSRF token. Please try again later.", null);
}
$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"]);
return $payload;
}
}

View File

@ -82,16 +82,11 @@ class Util
/**
* Generates an appropriate CSRF token.
*
* @param Logger $logger the logger to use if something goes wrong
* @return string|null the CSRF token, or `null` if no CSRF token could be created
* @return string the generated CSRF token
* @throws Exception if the CSRF token could not be generated
*/
static function generate_csrf_token(Logger $logger): ?string
static function generate_csrf_token(): string
{
try {
return bin2hex(random_bytes(32));
} catch (Exception $exception) {
$logger->emergency("Failed to generate token.", ["cause" => $exception]);
return null;
}
return bin2hex(random_bytes(32));
}
}

View File

@ -0,0 +1,34 @@
<?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

@ -0,0 +1,35 @@
<?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

@ -0,0 +1,27 @@
<?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

@ -0,0 +1,27 @@
<?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

@ -0,0 +1,44 @@
<?php
namespace com\fwdekker\deathnotifier\trackings;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\validator\HasLengthRule;
use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
class AddTrackingAction extends Action
{
private readonly TrackingManager $tracking_manager;
public function __construct(TrackingManager $tracking_manager)
{
parent::__construct(
ActionMethod::POST,
"add-tracking",
require_logged_in: true,
require_valid_csrf_token: true,
rule_lists: [
"person_name" => [
new IsNotBlankRule(),
new HasLengthRule(TrackingManager::MIN_TITLE_LENGTH, TrackingManager::MAX_TITLE_LENGTH)
],
],
);
$this->tracking_manager = $tracking_manager;
}
function handle(): mixed
{
$response = $this->tracking_manager->add_tracking($_SESSION["uuid"], $_POST["person_name"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace com\fwdekker\deathnotifier\trackings;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
class ListTrackingsAction extends Action
{
private readonly TrackingManager $tracking_manager;
public function __construct(TrackingManager $tracking_manager)
{
parent::__construct(
ActionMethod::GET,
"list-trackings",
require_logged_in: true,
require_valid_csrf_token: true,
);
$this->tracking_manager = $tracking_manager;
}
function handle(): mixed
{
$response = $this->tracking_manager->list_trackings($_SESSION["uuid"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace com\fwdekker\deathnotifier\trackings;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
class RemoveTrackingAction extends Action
{
private readonly TrackingManager $tracking_manager;
public function __construct(TrackingManager $tracking_manager)
{
parent::__construct(
ActionMethod::POST,
"remove-tracking",
require_logged_in: true,
require_valid_csrf_token: true,
rule_lists: ["person_name" => [new IsNotBlankRule()]],
);
$this->tracking_manager = $tracking_manager;
}
function handle(): mixed
{
$response = $this->tracking_manager->remove_tracking($_SESSION["uuid"], $_POST["person_name"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

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

View File

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

View File

@ -0,0 +1,36 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
class GetUserDataAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(
ActionMethod::GET,
"get-user-data",
require_logged_in: true,
require_valid_csrf_token: true,
);
$this->user_manager = $user_manager;
}
function handle(): mixed
{
$response = $this->user_manager->get_user($_SESSION["uuid"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
class LoginAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(
ActionMethod::POST,
"login",
require_logged_out: true,
require_valid_csrf_token: true,
rule_lists: ["email" => [new IsEmailRule()]],
);
$this->user_manager = $user_manager;
}
function handle(): mixed
{
[$response, $uuid] = $this->user_manager->check_login($_POST["email"], $_POST["password"]);
if ($response->satisfied) $_SESSION["uuid"] = $uuid;
return $response->payload;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\Util;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
use Exception;
use Monolog\Logger;
class LogoutAction extends Action
{
public function __construct()
{
parent::__construct(
ActionMethod::POST,
"logout",
require_logged_in: true,
require_valid_csrf_token: true,
);
}
function handle(): mixed
{
session_destroy();
session_start();
try {
$_SESSION["token"] = Util::generate_csrf_token();
} catch (Exception) {
throw new ActionException("Failed to generate new CSRF token. Please try again later.", null);
}
return null;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\validator\HasLengthRule;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
class RegisterAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(
ActionMethod::POST,
"register",
require_logged_out: true,
require_valid_csrf_token: true,
rule_lists: [
"email" => [new IsEmailRule()],
"password" => [new HasLengthRule(UserManager::MIN_PASSWORD_LENGTH, UserManager::MAX_PASSWORD_LENGTH)],
],
);
$this->user_manager = $user_manager;
}
function handle(): mixed
{
$response = $this->user_manager->register_user($_POST["email"], $_POST["password"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

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

View File

@ -0,0 +1,36 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
class ResendVerifyEmailAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(
ActionMethod::POST,
"resend-verify-email",
require_logged_in: true,
require_valid_csrf_token: true,
);
$this->user_manager = $user_manager;
}
function handle(): mixed
{
$response = $this->user_manager->resend_verify_email($_SESSION["uuid"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\validator\HasLengthRule;
class ResetPasswordAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(
ActionMethod::POST,
"reset-password",
require_valid_csrf_token: true,
rule_lists: [
"password" => [new HasLengthRule(UserManager::MIN_PASSWORD_LENGTH, UserManager::MAX_PASSWORD_LENGTH)],
],
);
$this->user_manager = $user_manager;
}
function handle(): mixed
{
$response = $this->user_manager->reset_password($_POST["email"], $_POST["reset_token"], $_POST["password"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

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

View File

@ -0,0 +1,37 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
class SendPasswordResetAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(
ActionMethod::POST,
"send-password-reset",
require_valid_csrf_token: true,
rule_lists: ["email" => [new IsEmailRule()]],
);
$this->user_manager = $user_manager;
}
function handle(): mixed
{
$response = $this->user_manager->send_password_reset($_POST["email"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
class ToggleNotificationsAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(
ActionMethod::POST,
"toggle-notifications",
require_logged_in: true,
require_valid_csrf_token: true,
);
$this->user_manager = $user_manager;
}
function handle(): mixed
{
$response = $this->user_manager->toggle_notifications($_SESSION["uuid"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
class UpdateEmailAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(
ActionMethod::POST,
"update-email",
require_logged_in: true,
require_valid_csrf_token: true,
rule_lists: ["email" => [new IsEmailRule()]],
);
$this->user_manager = $user_manager;
}
function handle(): mixed
{
$response = $this->user_manager->set_email($_SESSION["uuid"], $_POST["email"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\validator\HasLengthRule;
use com\fwdekker\deathnotifier\validator\IsSetRule;
class UpdatePasswordAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(
ActionMethod::POST,
"update-password",
require_logged_in: true,
require_valid_csrf_token: true,
rule_lists: [
"password_old" => [new IsSetRule()],
"password_new" => [
new HasLengthRule(UserManager::MIN_PASSWORD_LENGTH, UserManager::MAX_PASSWORD_LENGTH)
],
],
);
$this->user_manager = $user_manager;
}
function handle(): mixed
{
$response = $this->user_manager->set_password($_SESSION["uuid"], $_POST["password_old"], $_POST["password_new"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\Util;
use com\fwdekker\deathnotifier\validator\HasLengthRule;
use com\fwdekker\deathnotifier\validator\IsSetRule;
use Exception;
class UserDeleteAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(
ActionMethod::POST,
"user-delete",
require_logged_in: true,
require_valid_csrf_token: true,
rule_lists: [
"password_old" => [new IsSetRule()],
"password_new" => [
new HasLengthRule(UserManager::MIN_PASSWORD_LENGTH, UserManager::MAX_PASSWORD_LENGTH)
],
],
);
$this->user_manager = $user_manager;
}
function handle(): mixed
{
$response = $this->user_manager->delete_user($_SESSION["uuid"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
session_destroy();
session_start();
try {
$_SESSION["token"] = Util::generate_csrf_token();
} catch (Exception) {
throw new ActionException("Failed to generate new CSRF token. Please try again later.", null);
}
return $response->payload;
}
}

View File

@ -1,14 +1,10 @@
<?php
namespace com\fwdekker\deathnotifier;
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\mailer\ChangedEmailEmail;
use com\fwdekker\deathnotifier\mailer\ChangedPasswordEmail;
use com\fwdekker\deathnotifier\Database;
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 DateTime;
use com\fwdekker\deathnotifier\Response;
use Monolog\Logger;
use PDO;

View File

@ -0,0 +1,41 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
use com\fwdekker\deathnotifier\validator\IsSetRule;
class ValidatePasswordResetTokenAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(
ActionMethod::GET,
"validate-password-reset-token",
require_valid_csrf_token: true,
rule_lists: [
"reset_token" => [new IsSetRule()],
"email" => [new IsEmailRule()],
],
);
$this->user_manager = $user_manager;
}
function handle(): mixed
{
$response = $this->user_manager->validate_password_reset_token($_GET["email"], $_GET["reset_token"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\ActionException;
use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
use com\fwdekker\deathnotifier\validator\IsSetRule;
class VerifyEmailAction extends Action
{
private readonly UserManager $user_manager;
public function __construct(UserManager $user_manager)
{
parent::__construct(
ActionMethod::POST,
"verify-email",
require_valid_csrf_token: true,
rule_lists: [
"email" => [new IsEmailRule()],
"verify_token" => [new IsSetRule()],
],
);
$this->user_manager = $user_manager;
}
function handle(): mixed
{
$response = $this->user_manager->verify_email($_POST["email"], $_POST["verify_token"]);
if (!$response->satisfied)
throw new ActionException($response->payload["message"], $response->payload["target"]);
return $response->payload;
}
}

View File

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

View File

@ -2,13 +2,13 @@
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\Response;
use com\fwdekker\deathnotifier\ValidationException;
/**
* Requires the input to be of a specific length.
* Verifies that the input is of the specific length.
*/
class LengthRule extends Rule
class HasLengthRule extends Rule
{
/**
* @var int|null The minimum length (inclusive), or `null` if there is no minimum length.
@ -21,7 +21,7 @@ class LengthRule extends Rule
/**
* Instantiates a new rule.
* Verifies that the input is of the specific length.
*
* @param int|null $min_length the minimum length (inclusive), or `null` if there is no minimum length
* @param int|null $max_length the maximum length (inclusive), or `null` if there is no maximum length
@ -38,30 +38,27 @@ class LengthRule extends Rule
/**
* Checks whether the input is of the specified length.
* 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 string $key the key in `inputs` of the input to check
* @return Response|null `null` if the input is of the specified length, or an unsatisfied `Response` otherwise
* @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
*/
public function check(array $inputs, string $key): ?Response
public function check(array $inputs, string $key): void
{
if (!isset($inputs[$key]))
return Response::unsatisfied(
$this->override_message ?? "Missing input '$key'.",
$key
);
else if ($this->min_length !== null && strlen($inputs[$key]) < $this->min_length)
return Response::unsatisfied(
throw new ValidationException($this->override_message ?? "Missing input '$key'.", $key);
if ($this->min_length !== null && strlen($inputs[$key]) < $this->min_length)
throw new ValidationException(
$this->override_message ?? "Use at least $this->min_length character(s).",
$key
);
else if ($this->max_length !== null && strlen($inputs[$key]) > $this->max_length)
return Response::unsatisfied(
if ($this->max_length !== null && strlen($inputs[$key]) > $this->max_length)
throw new ValidationException(
$this->override_message ?? "Use at most $this->max_length character(s).",
$key
);
else
return null;
}
}

View File

@ -2,25 +2,25 @@
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\Response;
use com\fwdekker\deathnotifier\ValidationException;
/**
* Requires the input to be a valid email address.
* Verifies that the input is an email address.
*/
class IsEmailRule extends Rule
{
/**
* Checks whether the input is a valid email address.
* 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 string $key the key in `inputs` of the input to check
* @return Response|null `null` if `$inputs[$key]` is an email address, or an unsatisfied `Response` otherwise
* @return void if `$inputs[$key]` is an email address
* @throws ValidationException if `$inputs[$key]` is not set or is not an email address
*/
public function check(array $inputs, string $key): ?Response
public function check(array $inputs, string $key): void
{
return !isset($inputs[$key]) || !filter_var($inputs[$key], FILTER_VALIDATE_EMAIL)
? Response::unsatisfied($this->override_message ?? "Enter a valid email address.", $key)
: null;
if (!isset($inputs[$key]) || !filter_var($inputs[$key], FILTER_VALIDATE_EMAIL))
throw new ValidationException($this->override_message ?? "Enter a valid email address.", $key);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\ValidationException;
/**
* Verifies that the input has the specified value.
*/
class IsEqualToRule extends Rule
{
/**
* @var string the required value
*/
private readonly string $expected;
/**
* Constructs an `IsEqualToRule` that checks for equality with the specified value.
*
* @param string $expected the value that checked values should be equal to
*/
public function __construct(string $expected, ?string $override_message = null)
{
parent::__construct($override_message);
$this->expected = $expected;
}
/**
* 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 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`
*/
public function check(array $inputs, string $key): void
{
if (!isset($inputs[$key]) || $inputs[$key] !== $this->expected)
throw new ValidationException(
$this->override_message ?? "Inputs '$key' should equal '$this->expected'.",
$key
);
}
}

View File

@ -2,26 +2,25 @@
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\Response;
use com\fwdekker\deathnotifier\ValidationException;
/**
* Requires the input to not be blank.
* Verifies that the input is not blank.
*/
class IsNotBlankRule extends Rule
{
/**
* Checks whether the input is not blank.
* 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 string $key the key in `inputs` of the input to check
* @return Response|null `null` if `trim($inputs[$key])` is not an empty string, or an unsatisfied `Response`
* otherwise
* @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
*/
public function check(array $inputs, string $key): ?Response
public function check(array $inputs, string $key): void
{
return !isset($inputs[$key]) || trim($inputs[$key]) === ""
? Response::unsatisfied($this->override_message ?? "Use at least one character.", $key)
: null;
if (!isset($inputs[$key]) || trim($inputs[$key]) === "")
throw new ValidationException($this->override_message ?? "Use at least one character.", $key);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\ValidationException;
/**
* Verifies that the input is not set.
*/
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 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
*/
public function check(array $inputs, string $key): void
{
if (isset($inputs[$key]))
throw new ValidationException(
$this->override_message ?? "Field '" . htmlentities($key) . "' must not be set.",
$key
);
}
}

View File

@ -2,25 +2,28 @@
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\Response;
use com\fwdekker\deathnotifier\ValidationException;
/**
* Requires the input to be set.
* Verifies that the input is set.
*/
class IsSetRule extends Rule
{
/**
* Checks whether the input is set.
* 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 string $key the key in `inputs` of the input to check
* @return Response|null `null` if `isset($inputs[$key])`, or an unsatisfied `Response` otherwise
* @return void if `$inputs[$key]` is set
* @throws ValidationException if `$inputs[$key]` is not set
*/
public function check(array $inputs, string $key): ?Response
public function check(array $inputs, string $key): void
{
return !isset($inputs[$key])
? Response::unsatisfied($this->override_message ?? "Field '" . htmlentities($key) . "' required.", $key)
: null;
if (!isset($inputs[$key]))
throw new ValidationException(
$this->override_message ?? "Field '" . htmlentities($key) . "' must be set.",
$key
);
}
}

View File

@ -3,6 +3,7 @@
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\Response;
use com\fwdekker\deathnotifier\ValidationException;
/**
@ -36,7 +37,8 @@ abstract class Rule
*
* @param array<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 Response|null `null` if the rule holds, or an unsatisfied `Response` otherwise
* @return void if the rule holds
* @throws ValidationException if the rule does not hold
*/
public abstract function check(array $inputs, string $key): ?Response;
public abstract function check(array $inputs, string $key): void;
}

View File

@ -0,0 +1,19 @@
<?php
namespace com\fwdekker\deathnotifier;
use Exception;
class ValidationException extends Exception
{
public readonly ?string $target;
public function __construct(?string $message = null, ?string $target = null)
{
parent::__construct($message);
$this->target = $target;
}
}

View File

@ -1,76 +0,0 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\Response;
/**
* Validates arrays of inputs such as `$_POST` or `$_SESSION` using `Rule`s.
*/
class Validator
{
/**
* Validates whether values in `inputs` match the rules specified in `rule_sets`.
*
* @param array<string, string> $inputs the array of inputs in which to check the values
* @param array<string, Rule[]> $rule_sets maps keys in `inputs` to an array of `Rule`s to be checked
* @return Response|null `null` if all rules are satisfied, or an unsatisfied `Response` otherwise
*/
static function validate_inputs(array $inputs, array $rule_sets): ?Response
{
foreach ($rule_sets as $key => $rules) {
foreach ($rules as $rule) {
$is_valid = $rule->check($inputs, $key);
if ($is_valid !== null)
return $is_valid;
}
}
return null;
}
/**
* Validates that the user is logged in.
*
* @param array<string, string> $session the session to check
* @return Response|null `null` if the user is logged in, or an unsatisfied `Response` otherwise
*/
static function validate_logged_in(array $session): ?Response
{
if (!isset($session["uuid"]))
return Response::unsatisfied("You must be logged in to perform this action.");
return null;
}
/**
* Validates that the user is logged out.
*
* @param array<string, string> $session the session to check
* @return Response|null `null` if the user is logged out, or an unsatisfied `Response` otherwise
*/
static function validate_logged_out(array $session): ?Response
{
if (isset($session["uuid"]))
return Response::unsatisfied("You must be logged out to perform this action.");
return null;
}
/**
* Validates that the array contains the correct token.
*
* @param array<string, string> $token_array the array with key `token`
* @param string $token the expected token
* @return Response|null `null` if the token is correct, or an unsatisfied `Response` otherwise
*/
static function validate_token(array $token_array, string $token): ?Response
{
if (!isset($token_array["token"]) || $token_array["token"] !== $token)
return Response::unsatisfied("Invalid request token. Please refresh the page and try again.");
return null;
}
}

View File

@ -4,13 +4,13 @@ namespace com\fwdekker\deathnotifier\validator;
/**
* Unit tests for `LengthRule`.
* Unit tests for `HasLengthRule`.
*/
class LengthRuleTest extends RuleTest
{
function get_rule(?string $override = null): Rule
{
return new LengthRule(1, 6, $override);
return new HasLengthRule(1, 6, $override);
}
function get_valid_input(): ?string
@ -26,7 +26,7 @@ class LengthRuleTest extends RuleTest
public function test_returns_null_if_input_is_exactly_minimum_length(): void
{
$rule = new LengthRule(1, 3);
$rule = new HasLengthRule(1, 3);
$is_valid = $rule->check(["input" => "a"], "input");
@ -35,7 +35,7 @@ class LengthRuleTest extends RuleTest
public function test_returns_null_if_input_is_exactly_maximum_length(): void
{
$rule = new LengthRule(1, 3);
$rule = new HasLengthRule(1, 3);
$is_valid = $rule->check(["input" => "123"], "input");
@ -44,7 +44,7 @@ class LengthRuleTest extends RuleTest
public function test_returns_null_if_input_is_strictly_inside_range(): void
{
$rule = new LengthRule(1, 3);
$rule = new HasLengthRule(1, 3);
$is_valid = $rule->check(["input" => "12"], "input");
@ -53,7 +53,7 @@ class LengthRuleTest extends RuleTest
public function test_returns_not_null_if_input_is_strictly_below_minimum(): void
{
$rule = new LengthRule(1, 3);
$rule = new HasLengthRule(1, 3);
$is_valid = $rule->check(["input" => ""], "input");
@ -62,7 +62,7 @@ class LengthRuleTest extends RuleTest
public function test_returns_not_null_if_input_is_strictly_above_maximum(): void
{
$rule = new LengthRule(1, 3);
$rule = new HasLengthRule(1, 3);
$is_valid = $rule->check(["input" => "1234"], "input");