Make actions agnostic to the request method
And improve some exception structuring.
This commit is contained in:
parent
1cd9dfc9d2
commit
4c58b2a646
|
@ -60,6 +60,7 @@ $> mv dist/ /var/www/death-notifier/ # Move to public directory
|
|||
|
||||
Then, add the following lines to your crontab using `sudo -u www crontab -e`:
|
||||
```
|
||||
* * * * * cd /var/www/death-notifier && php /var/www/death-notifier/api.php process-email-queue secret_password
|
||||
*/5 * * * * cd /var/www/death-notifier && php /var/www/death-notifier/api.php update-all-trackings secret_password
|
||||
* * * * * cd /var/www/death-notifier && php /var/www/death-notifier/api.php action=process-email-queue password=secret_password
|
||||
*/5 * * * * cd /var/www/death-notifier && php /var/www/death-notifier/api.php action=update-all-trackings password=secret_password
|
||||
```
|
||||
It is recommended to also use a tool such as `newsyslog` to manage log rotation.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "fwdekker/death-notifier",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"version": "0.16.1", "_comment_version": "Also update version in `package.json`!",
|
||||
"version": "0.16.2", "_comment_version": "Also update version in `package.json`!",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"homepage": "https://git.fwdekker.com/tools/death-notifier",
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "death-notifier",
|
||||
"version": "0.16.1", "_comment_version": "Also update version in `composer.json`!",
|
||||
"version": "0.16.2", "_comment_version": "Also update version in `composer.json`!",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"author": "Florine W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
114
src/main/api.php
114
src/main/api.php
|
@ -5,6 +5,7 @@ use com\fwdekker\deathnotifier\ActionMethod;
|
|||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\EmulateCronAction;
|
||||
use com\fwdekker\deathnotifier\IllegalArgumentError;
|
||||
use com\fwdekker\deathnotifier\LoggerUtil;
|
||||
use com\fwdekker\deathnotifier\mailer\MailManager;
|
||||
use com\fwdekker\deathnotifier\mailer\ProcessEmailQueueAction;
|
||||
|
@ -36,73 +37,88 @@ use com\fwdekker\deathnotifier\Util;
|
|||
require_once __DIR__ . "/.vendor/autoload.php";
|
||||
|
||||
|
||||
// Preamble
|
||||
$logger = LoggerUtil::with_name();
|
||||
|
||||
$config = Config::get();
|
||||
if (hash_equals($config["admin"]["cli_secret"], "REPLACE THIS WITH A SECRET VALUE")) {
|
||||
$logger->error("You must set a CLI secret in the configuration file before running Death Notifier.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$db = new Database($config["database"]["filename"]);
|
||||
$mediawiki = new MediaWiki();
|
||||
$mail_manager = new MailManager($db->conn);
|
||||
$user_manager = new UserManager($db->conn, $mail_manager);
|
||||
$tracking_manager = new TrackingManager($db->conn);
|
||||
|
||||
|
||||
// Handle request
|
||||
// Wrap everything in try-catch to always return *something* to user
|
||||
try {
|
||||
session_start();
|
||||
$_SESSION["token"] = $_SESSION["token"] ?? Util::generate_csrf_token();
|
||||
$_POST = Util::parse_post();
|
||||
// Preamble
|
||||
$config = Config::get();
|
||||
if (hash_equals($config["admin"]["cli_secret"], "REPLACE THIS WITH A SECRET VALUE"))
|
||||
throw new IllegalArgumentError(
|
||||
"You must set a CLI secret in the configuration file before running Death Notifier."
|
||||
);
|
||||
|
||||
$db = new Database($config["database"]["filename"]);
|
||||
|
||||
$mediawiki = new MediaWiki();
|
||||
$mail_manager = new MailManager($db->conn);
|
||||
$user_manager = new UserManager($db->conn);
|
||||
$tracking_manager = new TrackingManager($db->conn);
|
||||
|
||||
// Update database
|
||||
$db->auto_install($mail_manager, $user_manager, $tracking_manager);
|
||||
$db->auto_migrate();
|
||||
|
||||
// Dispatch request
|
||||
session_start();
|
||||
$_SESSION["token"] = $_SESSION["token"] ?? Util::generate_csrf_token();
|
||||
|
||||
|
||||
// Set up request handlers
|
||||
$dispatcher = new ActionDispatcher();
|
||||
|
||||
// GET actions
|
||||
$dispatcher->register_action(new StartSessionAction($user_manager));
|
||||
$dispatcher->register_action(new GetPublicUserDataAction($user_manager));
|
||||
$dispatcher->register_action(new ListTrackingsAction($tracking_manager));
|
||||
$dispatcher->register_action(new ValidatePasswordResetTokenAction($user_manager));
|
||||
$dispatcher->register_actions([
|
||||
[ActionMethod::GET, "start-session", new StartSessionAction($user_manager)],
|
||||
[ActionMethod::GET, "get-user-data", new GetPublicUserDataAction($user_manager)],
|
||||
[ActionMethod::GET, "list-trackings", new ListTrackingsAction($tracking_manager)],
|
||||
|
||||
[ActionMethod::GET, "validate-password-reset-token", new ValidatePasswordResetTokenAction($user_manager)],
|
||||
]);
|
||||
|
||||
// POST actions
|
||||
$dispatcher->register_action(new RegisterAction($db->conn, $user_manager, $mail_manager));
|
||||
$dispatcher->register_action(new LoginAction($user_manager));
|
||||
$dispatcher->register_action(new LogoutAction());
|
||||
$dispatcher->register_action(new ResendVerifyEmailAction($db->conn, $user_manager, $mail_manager));
|
||||
$dispatcher->register_action(new VerifyEmailAction($db->conn, $user_manager, $mail_manager));
|
||||
$dispatcher->register_action(new ChangeEmailAction($db->conn, $user_manager, $mail_manager));
|
||||
$dispatcher->register_action(new ToggleNotificationsAction($db->conn, $user_manager));
|
||||
$dispatcher->register_action(new ChangePasswordAction($db->conn, $user_manager, $mail_manager));
|
||||
$dispatcher->register_action(new SendPasswordResetAction($db->conn, $user_manager, $mail_manager));
|
||||
$dispatcher->register_action(new ResetPasswordAction($db->conn, $user_manager, $mail_manager));
|
||||
$dispatcher->register_action(new UserDeleteAction($user_manager));
|
||||
$dispatcher->register_action(new AddTrackingAction($tracking_manager, $mediawiki));
|
||||
$dispatcher->register_action(new RemoveTrackingAction($tracking_manager));
|
||||
$dispatcher->register_actions([
|
||||
[ActionMethod::POST, "register", new RegisterAction($db->conn, $user_manager, $mail_manager)],
|
||||
[ActionMethod::POST, "login", new LoginAction($user_manager)],
|
||||
[ActionMethod::POST, "logout", new LogoutAction()],
|
||||
[ActionMethod::POST, "user-delete", new UserDeleteAction($user_manager)],
|
||||
|
||||
[ActionMethod::POST, "update-email", new ChangeEmailAction($db->conn, $user_manager, $mail_manager)],
|
||||
[ActionMethod::POST, "verify-email", new VerifyEmailAction($db->conn, $user_manager, $mail_manager)],
|
||||
[ActionMethod::POST, "resend-verify-email", new ResendVerifyEmailAction($db->conn, $user_manager, $mail_manager)],
|
||||
[ActionMethod::POST, "toggle-notifications", new ToggleNotificationsAction($db->conn, $user_manager)],
|
||||
|
||||
[ActionMethod::POST, "update-password", new ChangePasswordAction($db->conn, $user_manager, $mail_manager)],
|
||||
[ActionMethod::POST, "send-password-reset", new SendPasswordResetAction($db->conn, $user_manager, $mail_manager)],
|
||||
[ActionMethod::POST, "reset-password", new ResetPasswordAction($db->conn, $user_manager, $mail_manager)],
|
||||
|
||||
[ActionMethod::POST, "add-tracking", new AddTrackingAction($tracking_manager, $mediawiki)],
|
||||
[ActionMethod::POST, "remove-tracking", new RemoveTrackingAction($tracking_manager)],
|
||||
]);
|
||||
|
||||
// CLI actions
|
||||
$cli_actions = [
|
||||
new UpdateTrackingsAction($db->conn, $tracking_manager, $mediawiki, $mail_manager),
|
||||
new ProcessEmailQueueAction($mail_manager),
|
||||
];
|
||||
$dispatcher->register_action($cli_actions[0]);
|
||||
$dispatcher->register_action($cli_actions[1]);
|
||||
$dispatcher->register_action(new EmulateCronAction($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);
|
||||
$dispatcher->register_actions([
|
||||
[ActionMethod::CLI, "update-trackings", $cli_actions[0]],
|
||||
[ActionMethod::CLI, "process-email-queue", $cli_actions[1]],
|
||||
[ActionMethod::CLI, "emulate-cron", new EmulateCronAction($cli_actions)],
|
||||
]);
|
||||
|
||||
|
||||
// Handle request
|
||||
if (!isset($_SERVER["REQUEST_METHOD"]))
|
||||
$response = $dispatcher->handle(ActionMethod::CLI, Util::parse_cli());
|
||||
else if ($_SERVER["REQUEST_METHOD"] === "GET")
|
||||
$response = $dispatcher->handle(ActionMethod::GET, $_GET);
|
||||
else if ($_SERVER["REQUEST_METHOD"] === "POST")
|
||||
$response = $dispatcher->handle(ActionMethod::POST, Util::parse_post());
|
||||
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]);
|
||||
} catch (Throwable $exception) {
|
||||
$response = Response::unsatisfied("An unhandled error occurred. Please try again later.");
|
||||
$logger->error("An unhandled error occurred. Please try again later.", ["cause" => $exception]);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -577,11 +577,12 @@ doAfterLoad(() => {
|
|||
addTrackingForm.addEventListener("submit", (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const inputName = $("#add-tracking-name").value;
|
||||
postApi(
|
||||
{
|
||||
action: "add-tracking",
|
||||
token: csrfToken,
|
||||
person_name: $("#add-tracking-name").value,
|
||||
person_name: inputName,
|
||||
},
|
||||
addTrackingForm,
|
||||
(response: ServerResponse) => {
|
||||
|
@ -590,12 +591,12 @@ doAfterLoad(() => {
|
|||
|
||||
showMessageSuccess(
|
||||
addTrackingForm,
|
||||
response.payload["renamed"]
|
||||
inputName.toLowerCase() !== response.payload["normalized_name"].toLowerCase()
|
||||
? (
|
||||
`Successfully added <b>${response.payload["input"]}</b> as ` +
|
||||
`<b>${response.payload["name"]}</b>!`
|
||||
`Successfully added <b>${inputName}</b> as ` +
|
||||
`<b>${response.payload["normalized_name"]}</b>!`
|
||||
)
|
||||
: `Successfully added <b>${response.payload["name"]}</b>!`
|
||||
: `Successfully added <b>${response.payload["normalized_name"]}</b>!`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@ 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 com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
|
||||
|
@ -32,21 +32,10 @@ abstract class Action
|
|||
*/
|
||||
private readonly array $rule_lists;
|
||||
|
||||
/**
|
||||
* @var ActionMethod the method that this action handles
|
||||
*/
|
||||
public readonly ActionMethod $method;
|
||||
/**
|
||||
* @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
|
||||
|
@ -54,9 +43,7 @@ abstract class Action
|
|||
* @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 $name,
|
||||
bool $require_logged_in = false,
|
||||
public function __construct(bool $require_logged_in = false,
|
||||
bool $require_logged_out = false,
|
||||
bool $require_valid_csrf_token = false,
|
||||
array $rule_lists = [])
|
||||
|
@ -64,9 +51,6 @@ abstract class Action
|
|||
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->name = $name;
|
||||
|
||||
// TODO: Move authorisation-related validation to `dispatch` method?
|
||||
$this->require_logged_in = $require_logged_in;
|
||||
$this->require_logged_out = $require_logged_out;
|
||||
|
@ -78,13 +62,12 @@ abstract class Action
|
|||
/**
|
||||
* Validates inputs according to `rule_lists`, throwing an exception if any input is invalid.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs the inputs to validate
|
||||
* @return void if the input is valid
|
||||
* @throws ValidationException if the input is invalid
|
||||
* @throws InvalidInputException if the input is invalid
|
||||
*/
|
||||
function validate_inputs(): void
|
||||
public function validate_inputs(array $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)
|
||||
|
@ -101,9 +84,10 @@ abstract class Action
|
|||
/**
|
||||
* Performs the action.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs the inputs to perform the action with
|
||||
* @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
|
||||
* @throws InvalidInputException if the inputs are invalid upon further inspection
|
||||
*/
|
||||
abstract function handle(): mixed;
|
||||
abstract function handle(array $inputs): mixed;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier;
|
||||
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
|
||||
|
@ -23,46 +23,61 @@ class ActionDispatcher
|
|||
* 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 ActionMethod $method the request method that this action should handle
|
||||
* @param string $action_name the action name that this action should handle
|
||||
* @param Action $action the action to register
|
||||
* @return void
|
||||
*/
|
||||
public function register_action(Action $action): void
|
||||
public function register_action(ActionMethod $method, string $action_name, Action $action): void
|
||||
{
|
||||
$method = $action->method->name;
|
||||
$method_name = $method->name;
|
||||
|
||||
if (!isset($this->actions[$method]))
|
||||
$this->actions[$method] = [];
|
||||
if (!isset($this->actions[$method_name]))
|
||||
$this->actions[$method_name] = [];
|
||||
|
||||
if (isset($this->actions[$method][$action->name]))
|
||||
// TODO: Throw more specific exceptions(?)
|
||||
throw new InvalidArgumentException("Cannot register another handler for $method action '$action->name'.");
|
||||
if (isset($this->actions[$method_name][$action_name]))
|
||||
throw new IllegalArgumentError("Cannot register another handler for $method_name action '$action_name'.");
|
||||
|
||||
$this->actions[$method][$action->name] = $action;
|
||||
$this->actions[$method_name][$action_name] = $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method that invokes `#register_action` for each of the given `actions`.
|
||||
*
|
||||
* @param array<array{ActionMethod, string, Action}> $actions the actions to register, mapped from the request
|
||||
* method and action name that the action should handle
|
||||
* @return void
|
||||
*/
|
||||
public function register_actions(array $actions): void
|
||||
{
|
||||
foreach ($actions as $action)
|
||||
$this->register_action($action[0], $action[1], $action[2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the registered action for the given pair of method and action name.
|
||||
*
|
||||
* @param ActionMethod $method the method of the action to execute
|
||||
* @param array<int|string, mixed> $inputs the inputs to the action
|
||||
* @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
|
||||
public function handle(ActionMethod $method, array $inputs): Response
|
||||
{
|
||||
$inputs = $method->get_inputs();
|
||||
if (!isset($inputs["action"]))
|
||||
throw new InvalidArgumentException("No action specified.");
|
||||
throw new InvalidArgumentException("Malformed request: No action specified.");
|
||||
|
||||
$method_name = $method->name;
|
||||
$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'.");
|
||||
if (!isset($this->actions[$method_name]) || !isset($this->actions[$method_name][$action_name]))
|
||||
throw new InvalidArgumentException("Malformed request: Unknown $method_name action '$action_name'.");
|
||||
|
||||
$action = $this->actions[$method->name][$action_name];
|
||||
$action = $this->actions[$method_name][$action_name];
|
||||
try {
|
||||
$action->validate_inputs();
|
||||
$payload = $action->handle();
|
||||
$action->validate_inputs($inputs);
|
||||
$payload = $action->handle($inputs);
|
||||
return Response::satisfied($payload);
|
||||
} catch (ActionException|ValidationException $exception) {
|
||||
} catch (ActionException|InvalidInputException $exception) {
|
||||
return Response::unsatisfied($exception->getMessage(), $exception->target);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,19 +20,4 @@ enum ActionMethod
|
|||
* 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 => Util::parse_cli(),
|
||||
ActionMethod::GET => $_GET,
|
||||
ActionMethod::POST => $_POST,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ use InvalidArgumentException;
|
|||
*
|
||||
* Contains global state, but that's fine since it's read-only.
|
||||
*/
|
||||
// TODO: Override dynamically from tests
|
||||
class Config
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -63,7 +63,7 @@ class Database
|
|||
if ($stmt->fetch()[0] !== 0)
|
||||
return;
|
||||
|
||||
$this->logger->info("Database does not exist. Installing new database.");
|
||||
$this->logger->notice("Database does not exist. Installing new database.");
|
||||
|
||||
// Create `meta` table
|
||||
$this->conn->exec("CREATE TABLE meta(k TEXT NOT NULL UNIQUE PRIMARY KEY, v TEXT);");
|
||||
|
@ -76,7 +76,7 @@ class Database
|
|||
$user_manager->install();
|
||||
$tracking_manager->install();
|
||||
|
||||
$this->logger->info("Installation complete.");
|
||||
$this->logger->notice("Installation complete.");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ class Database
|
|||
if (Comparator::greaterThanOrEqualTo($db_version, Database::LATEST_VERSION))
|
||||
return;
|
||||
|
||||
$this->logger->info("Current db is v$db_version. Will migrate to v" . Database::LATEST_VERSION . ".");
|
||||
$this->logger->notice("Current db is v$db_version. Will migrate to v" . Database::LATEST_VERSION . ".");
|
||||
|
||||
// Get current version
|
||||
$stmt = $this->conn->prepare("SELECT v FROM meta WHERE k='version';");
|
||||
|
@ -113,7 +113,7 @@ class Database
|
|||
$stmt->bindValue(":version", Database::LATEST_VERSION);
|
||||
$stmt->execute();
|
||||
|
||||
$this->logger->info("Completed migration to v" . Database::LATEST_VERSION . ".");
|
||||
$this->logger->notice("Completed migration to v" . Database::LATEST_VERSION . ".");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -125,7 +125,7 @@ class Database
|
|||
*/
|
||||
private function migrate_0_5_0(): void
|
||||
{
|
||||
$this->logger->info("Migrating to v0.5.0.");
|
||||
$this->logger->notice("Migrating to v0.5.0.");
|
||||
|
||||
$this->conn->exec("ALTER TABLE users
|
||||
ADD COLUMN email_notifications_enabled INT NOT NULL DEFAULT(1);");
|
||||
|
@ -139,7 +139,7 @@ class Database
|
|||
*/
|
||||
private function migrate_0_8_0(): void
|
||||
{
|
||||
$this->logger->info("Migrating to v0.8.0.");
|
||||
$this->logger->notice("Migrating to v0.8.0.");
|
||||
|
||||
$this->conn->exec("CREATE TABLE new_email_tasks(type TEXT NOT NULL,
|
||||
recipient TEXT NOT NULL,
|
||||
|
@ -160,7 +160,7 @@ class Database
|
|||
*/
|
||||
private function migrate_0_10_0(): void
|
||||
{
|
||||
$this->logger->info("Migrating to v0.10.0.");
|
||||
$this->logger->notice("Migrating to v0.10.0.");
|
||||
|
||||
$this->conn->exec("CREATE TABLE new_email_tasks(type TEXT NOT NULL,
|
||||
recipient TEXT NOT NULL,
|
||||
|
@ -185,7 +185,7 @@ class Database
|
|||
*/
|
||||
private function migrate_0_16_0(): void
|
||||
{
|
||||
$this->logger->info("Migrating to v0.16.0.");
|
||||
$this->logger->notice("Migrating to v0.16.0.");
|
||||
|
||||
$this->conn->exec("DROP TABLE email_tasks;");
|
||||
$this->conn->exec("CREATE TABLE email_tasks(type_key TEXT NOT NULL,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier;
|
||||
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -30,7 +30,7 @@ class EmulateCronAction extends Action
|
|||
*/
|
||||
public function __construct(array $actions)
|
||||
{
|
||||
parent::__construct(ActionMethod::CLI, "emulate-cron");
|
||||
parent::__construct();
|
||||
|
||||
$this->actions = $actions;
|
||||
}
|
||||
|
@ -38,27 +38,29 @@ class EmulateCronAction extends Action
|
|||
/**
|
||||
* Validates the inputs of each registered action.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs the inputs to validate
|
||||
* @return void if the input is valid
|
||||
* @throws ValidationException if the input is invalid
|
||||
* @throws InvalidInputException if the input is invalid
|
||||
*/
|
||||
public function validate_inputs(): void
|
||||
public function validate_inputs(array $inputs): void
|
||||
{
|
||||
foreach ($this->actions as $action)
|
||||
$action->validate_inputs();
|
||||
$action->validate_inputs($inputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all trackings and processes the mail queue at a regular interval.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs ignored
|
||||
* @return never
|
||||
*/
|
||||
public function handle(): never
|
||||
public function handle(array $inputs): never
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
while (true) {
|
||||
print("Emulating cron jobs.\n");
|
||||
foreach ($this->actions as $action)
|
||||
$action->handle();
|
||||
$action->handle($inputs);
|
||||
print("Done.\n");
|
||||
|
||||
sleep(self::INTERVAL);
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier;
|
||||
|
||||
use Error;
|
||||
|
||||
|
||||
/**
|
||||
* Thrown if a function receives an argument which it cannot handle.
|
||||
*
|
||||
* This is an error, so it always indicates a bug in the program.
|
||||
*/
|
||||
class IllegalArgumentError extends Error
|
||||
{
|
||||
// Intentionally left empty
|
||||
}
|
|
@ -6,9 +6,11 @@ use Error;
|
|||
|
||||
|
||||
/**
|
||||
* Thrown if something happens that should not be able to happen, and there is no way to recover.
|
||||
* Thrown if something happens that should not be able to happen.
|
||||
*
|
||||
* This is an error, so it always indicates a bug in the program.
|
||||
*/
|
||||
class IllegalStateException extends Error
|
||||
class IllegalStateError extends Error
|
||||
{
|
||||
// Intentionally left empty
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier;
|
||||
|
||||
use Exception;
|
||||
|
||||
|
||||
/**
|
||||
* Thrown to indicate that a request to the server was malformed in some way.
|
||||
*
|
||||
* This is an exception, not an error, so it indicates that the client that sent the request did something wrong.
|
||||
*/
|
||||
class MalformedRequestException extends Exception
|
||||
{
|
||||
// Intentionally left empty
|
||||
}
|
|
@ -24,7 +24,7 @@ class StartSessionAction extends Action
|
|||
*/
|
||||
public function __construct(UserManager $user_manager)
|
||||
{
|
||||
parent::__construct(ActionMethod::GET, "start-session");
|
||||
parent::__construct();
|
||||
|
||||
$this->user_manager = $user_manager;
|
||||
}
|
||||
|
@ -33,11 +33,12 @@ class StartSessionAction extends Action
|
|||
/**
|
||||
* Starts a new user session, or continues an existing one.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs ignored
|
||||
* @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
|
||||
function handle(array $inputs): array
|
||||
{
|
||||
$config = Config::get();
|
||||
$payload = [];
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier;
|
||||
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use Exception;
|
||||
use JsonException;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -13,17 +15,24 @@ class Util
|
|||
/**
|
||||
* Parses POST values from JSON-based inputs.
|
||||
*
|
||||
* @return array<int|string, mixed>|null the parsed POST-ed values
|
||||
* @return array<int|string, mixed> the parsed POSTed values, or an empty array if no values were POSTed
|
||||
* @throws InvalidInputException if there are no POST input values, or if the POST input is not valid JSON
|
||||
*/
|
||||
static function parse_post(): ?array
|
||||
static function parse_post(): array
|
||||
{
|
||||
$output = $_POST;
|
||||
|
||||
$post_input = file_get_contents("php://input");
|
||||
if ($post_input !== false)
|
||||
$output = json_decode($post_input, associative: true);
|
||||
if ($post_input === false || trim($post_input) === "")
|
||||
return [];
|
||||
|
||||
return $output;
|
||||
try {
|
||||
$post = json_decode($post_input, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
if ($post === null)
|
||||
throw new InvalidInputException("Malformed request: POST data is `null`.");
|
||||
|
||||
return $post;
|
||||
} catch (JsonException) {
|
||||
throw new InvalidInputException("Malformed request: POST data could not be parsed as JSON.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,12 +40,13 @@ class Util
|
|||
*
|
||||
* Code from https://www.php.net/manual/en/features.commandline.php#108883.
|
||||
*
|
||||
* @return array<int|string, mixed> the parsed CLI inputs
|
||||
* @return array<int|string, mixed> the parsed CLI inputs, or an empty array if no values were passed on the CLI
|
||||
*/
|
||||
static function parse_cli(): array
|
||||
{
|
||||
parse_str(implode("&", array_slice($_SERVER["argv"], 1)), $output);
|
||||
if (!isset($_SERVER["argc"]) || $_SERVER["argc"] <= 1) return [];
|
||||
|
||||
parse_str(implode("&", array_slice($_SERVER["argv"], 1)), $output);
|
||||
return $output;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ namespace com\fwdekker\deathnotifier\mailer;
|
|||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionException;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\LoggerUtil;
|
||||
use com\fwdekker\deathnotifier\validator\IsEqualToRule;
|
||||
|
@ -37,8 +36,6 @@ class ProcessEmailQueueAction extends Action
|
|||
public function __construct(MailManager $mail_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::CLI,
|
||||
"process-email-queue",
|
||||
rule_lists: [
|
||||
"password" => [new IsEqualToRule(Config::get()["admin"]["cli_secret"], "Incorrect password.")]
|
||||
],
|
||||
|
@ -52,10 +49,11 @@ class ProcessEmailQueueAction extends Action
|
|||
/**
|
||||
* Processes the queue.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs ignored
|
||||
* @return null
|
||||
* @throws ActionException if the mailer could not be created or if an email could not be sent
|
||||
*/
|
||||
public function handle(): mixed
|
||||
public function handle(array $inputs): mixed
|
||||
{
|
||||
$emails = $this->mail_manager->get_queue();
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace com\fwdekker\deathnotifier\mediawiki;
|
|||
|
||||
use com\fwdekker\deathnotifier\LoggerUtil;
|
||||
use com\fwdekker\deathnotifier\validator\Rule;
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use Monolog\Logger;
|
||||
|
||||
|
||||
|
@ -45,18 +45,18 @@ class IsPersonPageRule extends Rule
|
|||
* @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]` refers to a page about a person on Wikipedia
|
||||
* @throws ValidationException if `$inputs[$key]` is not set or does not refer to a page about a person on Wikipedia
|
||||
* @throws InvalidInputException if `$inputs[$key]` is not set or does not refer to a page about a person on Wikipedia
|
||||
*/
|
||||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!isset($inputs[$key])) throw new ValidationException($this->override_message ?? "Field must be set.", $key);
|
||||
if (!isset($inputs[$key])) throw new InvalidInputException($this->override_message ?? "Field must be set.", $key);
|
||||
|
||||
$person_name = $inputs[$key];
|
||||
try {
|
||||
$info = $this->mediawiki->query_person_info([$person_name]);
|
||||
} catch (MediaWikiException $exception) {
|
||||
$this->logger->error("Failed to query page info.", ["cause" => $exception, "name" => $person_name]);
|
||||
throw new ValidationException(
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ?? "Could not reach Wikipedia. Maybe the website is down?"
|
||||
);
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ class IsPersonPageRule extends Rule
|
|||
$type = $info->results[$normalized_name]["type"];
|
||||
|
||||
if (in_array($normalized_name, $info->missing))
|
||||
throw new ValidationException(
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ??
|
||||
"Wikipedia does not have an article about " .
|
||||
"<b><a href='https://en.wikipedia.org/wiki/Special:Search?search=" .
|
||||
|
@ -75,7 +75,7 @@ class IsPersonPageRule extends Rule
|
|||
);
|
||||
|
||||
if ($type === ArticleType::Disambiguation)
|
||||
throw new ValidationException(
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ??
|
||||
"<b><a href='https://en.wikipedia.org/wiki/" . rawurlencode($normalized_name) . "'>" .
|
||||
htmlentities($normalized_name) . "</a></b> refers to multiple articles. " .
|
||||
|
@ -85,7 +85,7 @@ class IsPersonPageRule extends Rule
|
|||
);
|
||||
|
||||
if ($type === ArticleType::Other)
|
||||
throw new ValidationException(
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ??
|
||||
"The Wikipedia article about " .
|
||||
"<b><a href='https://en.wikipedia.org/wiki/" . rawurlencode($normalized_name) . "'>" .
|
||||
|
|
|
@ -4,15 +4,14 @@ namespace com\fwdekker\deathnotifier\tracking;
|
|||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionException;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\IllegalStateException;
|
||||
use com\fwdekker\deathnotifier\IllegalStateError;
|
||||
use com\fwdekker\deathnotifier\LoggerUtil;
|
||||
use com\fwdekker\deathnotifier\mediawiki\IsPersonPageRule;
|
||||
use com\fwdekker\deathnotifier\mediawiki\MediaWiki;
|
||||
use com\fwdekker\deathnotifier\mediawiki\MediaWikiException;
|
||||
use com\fwdekker\deathnotifier\validator\HasLengthRule;
|
||||
use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use Monolog\Logger;
|
||||
|
||||
|
||||
|
@ -44,8 +43,6 @@ class AddTrackingAction extends Action
|
|||
public function __construct(TrackingManager $tracking_manager, MediaWiki $mediawiki)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::POST,
|
||||
"add-tracking",
|
||||
require_logged_in: true,
|
||||
require_valid_csrf_token: true,
|
||||
rule_lists: [
|
||||
|
@ -64,16 +61,18 @@ class AddTrackingAction extends Action
|
|||
|
||||
|
||||
/**
|
||||
* Adds a tracking by the current user of `$_POST["person_name"]`.
|
||||
* Adds a tracking by the current user of the specified person.
|
||||
*
|
||||
* @return array{"name": string, "input": string, "renamed": bool} the
|
||||
* @param array<int|string, mixed> $inputs `"person_name": string`: the name of the person to track
|
||||
* @return array{"input_name": string, "normalized_name": string} the person's name as input by the user, and the
|
||||
* normalized version of that name
|
||||
* @throws ActionException if the Wikipedia API could not be reached
|
||||
* @throws ValidationException if the user is already tracking this person
|
||||
* @throws InvalidInputException if the user is already tracking this person
|
||||
*/
|
||||
function handle(): array
|
||||
public function handle(array $inputs): array
|
||||
{
|
||||
$user_uuid = $_SESSION["uuid"];
|
||||
$person_name = strval($_POST["person_name"]);
|
||||
$person_name = strval($inputs["person_name"]);
|
||||
|
||||
// Query API
|
||||
try {
|
||||
|
@ -86,19 +85,18 @@ class AddTrackingAction extends Action
|
|||
$normalized_name = $info->redirects[$person_name];
|
||||
$status = $info->results[$normalized_name]["status"];
|
||||
if ($status === null)
|
||||
throw new IllegalStateException("Article is about person, but person has no status.");
|
||||
throw new IllegalStateError("Article is about person, but person has no status.");
|
||||
|
||||
// Add tracking (Transaction is not necessary)
|
||||
if ($this->tracking_manager->has_tracking($user_uuid, $normalized_name))
|
||||
throw new ValidationException("You are already tracking <b>$normalized_name</b>.");
|
||||
throw new InvalidInputException("You are already tracking <b>$normalized_name</b>.");
|
||||
|
||||
$this->tracking_manager->add_tracking($user_uuid, $normalized_name, $status);
|
||||
|
||||
// Respond
|
||||
return [
|
||||
"name" => $normalized_name,
|
||||
"input" => $person_name,
|
||||
"renamed" => strtolower($person_name) !== strtolower($normalized_name)
|
||||
"input_name" => $person_name,
|
||||
"normalized_name" => $normalized_name,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
namespace com\fwdekker\deathnotifier\tracking;
|
||||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -25,8 +24,6 @@ class ListTrackingsAction extends Action
|
|||
public function __construct(TrackingManager $tracking_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::GET,
|
||||
"list-trackings",
|
||||
require_logged_in: true,
|
||||
require_valid_csrf_token: true,
|
||||
);
|
||||
|
@ -38,9 +35,10 @@ class ListTrackingsAction extends Action
|
|||
/**
|
||||
* Returns all trackings of the current user.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs ignored
|
||||
* @return mixed[] all trackings of the current user
|
||||
*/
|
||||
function handle(): array
|
||||
public function handle(array $inputs): array
|
||||
{
|
||||
return $this->tracking_manager->list_trackings($_SESSION["uuid"]);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
namespace com\fwdekker\deathnotifier\tracking;
|
||||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
|
||||
|
||||
|
||||
|
@ -26,8 +25,6 @@ class RemoveTrackingAction extends Action
|
|||
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()]],
|
||||
|
@ -38,13 +35,14 @@ class RemoveTrackingAction extends Action
|
|||
|
||||
|
||||
/**
|
||||
* Removes the current user's tracking of `$_POST["person_name"]`.
|
||||
* Removes the current user's tracking of the specified person.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs `"person_name": string`: the name of the person to stop tracking
|
||||
* @return null
|
||||
*/
|
||||
function handle(): mixed
|
||||
public function handle(array $inputs): mixed
|
||||
{
|
||||
$this->tracking_manager->remove_tracking($_SESSION["uuid"], $_POST["person_name"]);
|
||||
$this->tracking_manager->remove_tracking($_SESSION["uuid"], $inputs["person_name"]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ namespace com\fwdekker\deathnotifier\tracking;
|
|||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionException;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\LoggerUtil;
|
||||
|
@ -55,8 +54,6 @@ class UpdateTrackingsAction extends Action
|
|||
public function __construct(PDO $conn, TrackingManager $tracking_manager, MediaWiki $mediawiki, MailManager $mailer)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::CLI,
|
||||
"update-trackings",
|
||||
rule_lists: [
|
||||
"password" => [new IsEqualToRule(Config::get()["admin"]["cli_secret"], "Incorrect password.")]
|
||||
],
|
||||
|
@ -73,10 +70,11 @@ class UpdateTrackingsAction extends Action
|
|||
/**
|
||||
* Updates all trackings that users have added.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs ignored
|
||||
* @return null
|
||||
* @throws ActionException if the Wikipedia API could not be reached
|
||||
*/
|
||||
public function handle(): mixed
|
||||
public function handle(array $inputs): mixed
|
||||
{
|
||||
$names = $this->tracking_manager->list_all_unique_person_names();
|
||||
if (empty($names)) return null;
|
||||
|
|
|
@ -4,13 +4,12 @@ namespace com\fwdekker\deathnotifier\user;
|
|||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionException;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
use com\fwdekker\deathnotifier\mailer\MailManager;
|
||||
use com\fwdekker\deathnotifier\validator\IsEmailRule;
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use PDO;
|
||||
|
||||
|
||||
|
@ -43,8 +42,6 @@ class ChangeEmailAction extends Action
|
|||
public function __construct(PDO $conn, UserManager $user_manager, MailManager $mail_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::POST,
|
||||
"update-email",
|
||||
require_logged_in: true,
|
||||
require_valid_csrf_token: true,
|
||||
rule_lists: ["email" => [new IsEmailRule()]],
|
||||
|
@ -59,23 +56,24 @@ class ChangeEmailAction extends Action
|
|||
/**
|
||||
* Changes the user's email address.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs `"email": string`: the email address to change to
|
||||
* @return null
|
||||
* @throws ValidationException if the email address is not different or the email address is already used
|
||||
* @throws InvalidInputException if the email address is not different or the email address is already used
|
||||
*/
|
||||
function handle(): mixed
|
||||
function handle(array $inputs): mixed
|
||||
{
|
||||
Database::transaction($this->conn, function () {
|
||||
Database::transaction($this->conn, function () use ($inputs) {
|
||||
$user_data = $this->user_manager->get_user_by_uuid($_SESSION["uuid"]);
|
||||
if ($user_data === null)
|
||||
throw new ActionException("Failed to retrieve account data. Refresh the page and try again.");
|
||||
if ($_POST["email"] === $user_data["email"])
|
||||
throw new ValidationException("That is already your email address.", "email");
|
||||
if ($this->user_manager->has_user_with_email($_POST["email"]))
|
||||
throw new ValidationException("That email address is already in use by someone else.", "email");
|
||||
if ($inputs["email"] === $user_data["email"])
|
||||
throw new InvalidInputException("That is already your email address.", "email");
|
||||
if ($this->user_manager->has_user_with_email($inputs["email"]))
|
||||
throw new InvalidInputException("That email address is already in use by someone else.", "email");
|
||||
|
||||
$token = $this->user_manager->set_email($_SESSION["uuid"], $_POST["email"]);
|
||||
$token = $this->user_manager->set_email($_SESSION["uuid"], $inputs["email"]);
|
||||
// TODO: Also send email to old email address
|
||||
$this->mail_manager->queue_email(new ChangeEmailEmail($_POST["email"], $token));
|
||||
$this->mail_manager->queue_email(new ChangeEmailEmail($inputs["email"], $token));
|
||||
});
|
||||
|
||||
return null;
|
||||
|
|
|
@ -4,14 +4,13 @@ namespace com\fwdekker\deathnotifier\user;
|
|||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionException;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
use com\fwdekker\deathnotifier\mailer\MailManager;
|
||||
use com\fwdekker\deathnotifier\validator\HasLengthRule;
|
||||
use com\fwdekker\deathnotifier\validator\IsSetRule;
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use PDO;
|
||||
|
||||
|
||||
|
@ -44,8 +43,6 @@ class ChangePasswordAction extends Action
|
|||
public function __construct(PDO $conn, UserManager $user_manager, MailManager $mail_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::POST,
|
||||
"update-password",
|
||||
require_logged_in: true,
|
||||
require_valid_csrf_token: true,
|
||||
rule_lists: [
|
||||
|
@ -65,19 +62,21 @@ class ChangePasswordAction extends Action
|
|||
/**
|
||||
* Changes the user's password.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs `"password_old": string`: the old password, `"password_new": string`: the
|
||||
* new password
|
||||
* @return null
|
||||
* @throws ValidationException if the old password is incorrect
|
||||
* @throws InvalidInputException if the old password is incorrect
|
||||
*/
|
||||
function handle(): mixed
|
||||
function handle(array $inputs): mixed
|
||||
{
|
||||
Database::transaction($this->conn, function () {
|
||||
Database::transaction($this->conn, function () use ($inputs) {
|
||||
$user_data = $this->user_manager->get_user_by_uuid($_SESSION["uuid"]);
|
||||
if ($user_data === null)
|
||||
throw new ActionException("Failed to retrieve account data. Refresh the page and try again.");
|
||||
if (!password_verify($user_data["password"], $_POST["password_old"]))
|
||||
throw new ValidationException("Incorrect old password.", "password_old");
|
||||
if (!password_verify($user_data["password"], $inputs["password_old"]))
|
||||
throw new InvalidInputException("Incorrect old password.", "password_old");
|
||||
|
||||
$this->user_manager->set_password($_SESSION["uuid"], $_POST["password_new"]);
|
||||
$this->user_manager->set_password($_SESSION["uuid"], $inputs["password_new"]);
|
||||
$this->mail_manager->queue_email(new ChangePasswordEmail($user_data["email"]));
|
||||
});
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ namespace com\fwdekker\deathnotifier\user;
|
|||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionException;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -25,12 +24,7 @@ class GetPublicUserDataAction extends Action
|
|||
*/
|
||||
public function __construct(UserManager $user_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::GET,
|
||||
"get-user-data",
|
||||
require_logged_in: true,
|
||||
require_valid_csrf_token: true,
|
||||
);
|
||||
parent::__construct(require_logged_in: true, require_valid_csrf_token: true);
|
||||
|
||||
$this->user_manager = $user_manager;
|
||||
}
|
||||
|
@ -39,11 +33,12 @@ class GetPublicUserDataAction extends Action
|
|||
/**
|
||||
* Returns the user's public data.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs ignored
|
||||
* @return array{"email": string, "email_verified": bool, "email_notifications_enabled": bool,
|
||||
* "password_last_change": int} the user's public data
|
||||
* @throws ActionException if the user's data could not be retrieved
|
||||
*/
|
||||
function handle(): array
|
||||
function handle(array $inputs): array
|
||||
{
|
||||
$user_data = $this->user_manager->get_user_by_uuid($_SESSION["uuid"]);
|
||||
if ($user_data === null)
|
||||
|
|
|
@ -4,8 +4,8 @@ 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;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -27,11 +27,12 @@ class LoginAction extends Action
|
|||
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()]],
|
||||
rule_lists: [
|
||||
"email" => [new IsEmailRule()],
|
||||
"password" => [new IsSetRule()],
|
||||
],
|
||||
);
|
||||
|
||||
$this->user_manager = $user_manager;
|
||||
|
@ -41,15 +42,17 @@ class LoginAction extends Action
|
|||
/**
|
||||
* Logs in the user.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs `"email": string`: the email to log in with, `"password": string`: the
|
||||
* password to log in with
|
||||
* @return null
|
||||
* @throws ActionException if the user's data could not be retrieved or if the given credentials are incorrect
|
||||
*/
|
||||
function handle(): mixed
|
||||
function handle(array $inputs): mixed
|
||||
{
|
||||
$user_data = $this->user_manager->get_user_by_email($_POST["email"]);
|
||||
$user_data = $this->user_manager->get_user_by_email($inputs["email"]);
|
||||
if ($user_data === null)
|
||||
throw new ActionException("Failed to retrieve account data. Refresh the page and try again.");
|
||||
if (!password_verify($_POST["password"], $user_data["password"]))
|
||||
throw new ActionException("Incorrect combination of email and password.", "password");
|
||||
if (!password_verify($inputs["password"], $user_data["password"]))
|
||||
throw new ActionException("Incorrect combination of email and password.", "password");
|
||||
|
||||
$_SESSION["uuid"] = $user_data["uuid"];
|
||||
|
|
|
@ -4,7 +4,6 @@ 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 Exception;
|
||||
|
||||
|
@ -19,22 +18,18 @@ class LogoutAction extends Action
|
|||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::POST,
|
||||
"logout",
|
||||
require_logged_in: true,
|
||||
require_valid_csrf_token: true,
|
||||
);
|
||||
parent::__construct(require_logged_in: true, require_valid_csrf_token: true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Terminates the current user session.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs ignored
|
||||
* @return null
|
||||
* @throws ActionException if no CSRF token could be generated
|
||||
*/
|
||||
function handle(): mixed
|
||||
function handle(array $inputs): mixed
|
||||
{
|
||||
session_destroy();
|
||||
session_start();
|
||||
|
|
|
@ -4,14 +4,13 @@ namespace com\fwdekker\deathnotifier\user;
|
|||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionException;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
use com\fwdekker\deathnotifier\mailer\MailManager;
|
||||
use com\fwdekker\deathnotifier\validator\HasLengthRule;
|
||||
use com\fwdekker\deathnotifier\validator\IsEmailRule;
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use PDO;
|
||||
|
||||
|
||||
|
@ -44,8 +43,6 @@ class RegisterAction extends Action
|
|||
public function __construct(PDO $conn, UserManager $user_manager, MailManager $mail_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::POST,
|
||||
"register",
|
||||
require_logged_out: true,
|
||||
require_valid_csrf_token: true,
|
||||
rule_lists: [
|
||||
|
@ -63,22 +60,24 @@ class RegisterAction extends Action
|
|||
/**
|
||||
* Registers a new user.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs `"email": string`: the email address to register, `"password": string`:
|
||||
* the password to use for the new account
|
||||
* @return null
|
||||
* @throws ValidationException if the email address is already used
|
||||
* @throws InvalidInputException if the email address is already used
|
||||
*/
|
||||
function handle(): mixed
|
||||
function handle(array $inputs): mixed
|
||||
{
|
||||
Database::transaction($this->conn, function () {
|
||||
if ($this->user_manager->has_user_with_email($_POST["email"]))
|
||||
throw new ValidationException("That email address already in use by someone else.", "email");
|
||||
Database::transaction($this->conn, function () use ($inputs) {
|
||||
if ($this->user_manager->has_user_with_email($inputs["email"]))
|
||||
throw new InvalidInputException("That email address already in use by someone else.", "email");
|
||||
|
||||
$uuid = $this->user_manager->add_user($_POST["email"], $_POST["password"]);
|
||||
$uuid = $this->user_manager->add_user($inputs["email"], $inputs["password"]);
|
||||
$user_data = $this->user_manager->get_user_by_uuid($uuid);
|
||||
if ($user_data === null)
|
||||
throw new ActionException("Failed to retrieve account data. Refresh the page and try again.");
|
||||
$token = $user_data["email_verification_token"];
|
||||
|
||||
$this->mail_manager->queue_email(new RegisterEmail($_POST["email"], $token));
|
||||
$this->mail_manager->queue_email(new RegisterEmail($inputs["email"], $token));
|
||||
});
|
||||
|
||||
return null;
|
||||
|
|
|
@ -3,13 +3,12 @@
|
|||
namespace com\fwdekker\deathnotifier\user;
|
||||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
use com\fwdekker\deathnotifier\mailer\MailManager;
|
||||
use com\fwdekker\deathnotifier\Util;
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use PDO;
|
||||
|
||||
|
||||
|
@ -41,12 +40,7 @@ class ResendVerifyEmailAction extends Action
|
|||
*/
|
||||
public function __construct(PDO $conn, UserManager $user_manager, MailManager $mail_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::POST,
|
||||
"resend-verify-email",
|
||||
require_logged_in: true,
|
||||
require_valid_csrf_token: true,
|
||||
);
|
||||
parent::__construct(require_logged_in: true, require_valid_csrf_token: true);
|
||||
|
||||
$this->conn = $conn;
|
||||
$this->user_manager = $user_manager;
|
||||
|
@ -57,23 +51,24 @@ class ResendVerifyEmailAction extends Action
|
|||
/**
|
||||
* Resets the email verification process and sends a new verification email.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs ignored
|
||||
* @return null
|
||||
* @throws ValidationException if the email address is already verified or if a verification email was sent too
|
||||
* @throws InvalidInputException if the email address is already verified or if a verification email was sent too
|
||||
* recently
|
||||
*/
|
||||
function handle(): mixed
|
||||
function handle(array $inputs): mixed
|
||||
{
|
||||
Database::transaction($this->conn, function () {
|
||||
Database::transaction($this->conn, function () use ($inputs) {
|
||||
$user_data = $this->user_manager->get_user_by_uuid($_SESSION["uuid"]);
|
||||
if ($user_data["email_verification_token"] === null)
|
||||
throw new ValidationException("Your email address is already verified.");
|
||||
throw new InvalidInputException("Your email address is already verified.");
|
||||
|
||||
$minutes_left = Util::minutes_until_interval_elapsed(
|
||||
$user_data["email_verification_token_timestamp"],
|
||||
UserManager::MINUTES_BETWEEN_VERIFICATION_EMAILS
|
||||
);
|
||||
if ($minutes_left > 0) {
|
||||
throw new ValidationException(
|
||||
throw new InvalidInputException(
|
||||
"A verification email was sent recently. " .
|
||||
"Please wait $minutes_left more minute(s) before requesting a new email."
|
||||
);
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
namespace com\fwdekker\deathnotifier\user;
|
||||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
use com\fwdekker\deathnotifier\mailer\MailManager;
|
||||
use com\fwdekker\deathnotifier\validator\HasLengthRule;
|
||||
use com\fwdekker\deathnotifier\validator\IsEmailRule;
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use com\fwdekker\deathnotifier\validator\IsSetRule;
|
||||
use PDO;
|
||||
|
||||
|
||||
|
@ -43,12 +43,11 @@ class ResetPasswordAction extends Action
|
|||
public function __construct(PDO $conn, UserManager $user_manager, MailManager $mail_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::POST,
|
||||
"reset-password",
|
||||
require_valid_csrf_token: true,
|
||||
rule_lists: [
|
||||
"email" => [new IsEmailRule()],
|
||||
"password" => [new HasLengthRule(UserManager::MIN_PASSWORD_LENGTH, UserManager::MAX_PASSWORD_LENGTH)],
|
||||
"reset_token" => [new IsSetRule()],
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -61,19 +60,21 @@ class ResetPasswordAction extends Action
|
|||
/**
|
||||
* Resets the user's password after they forgot it.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs `"email": string`: the email address of the account to reset the password
|
||||
* of, `"password": string`: the new password to set, `"reset_token": string`: the token to reset the password with
|
||||
* @return null
|
||||
*/
|
||||
function handle(): mixed
|
||||
function handle(array $inputs): mixed
|
||||
{
|
||||
Database::transaction($this->conn, function () {
|
||||
$user_data = $this->user_manager->get_user_by_email($_POST["email"]);
|
||||
if ($_GET["reset_token"] !== $user_data["password_reset_token"])
|
||||
throw new ValidationException(
|
||||
Database::transaction($this->conn, function () use ($inputs) {
|
||||
$user_data = $this->user_manager->get_user_by_email($inputs["email"]);
|
||||
if ($inputs["reset_token"] !== $user_data["password_reset_token"])
|
||||
throw new InvalidInputException(
|
||||
"This password reset link is invalid. Maybe you already reset your password?"
|
||||
);
|
||||
|
||||
$this->user_manager->set_password($user_data["uuid"], $_POST["password"]);
|
||||
$this->mail_manager->queue_email(new ResetPasswordEmail($_POST["email"]));
|
||||
$this->user_manager->set_password($user_data["uuid"], $inputs["password"]);
|
||||
$this->mail_manager->queue_email(new ResetPasswordEmail($inputs["email"]));
|
||||
});
|
||||
|
||||
return null;
|
||||
|
|
|
@ -3,14 +3,13 @@
|
|||
namespace com\fwdekker\deathnotifier\user;
|
||||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
use com\fwdekker\deathnotifier\mailer\MailManager;
|
||||
use com\fwdekker\deathnotifier\Util;
|
||||
use com\fwdekker\deathnotifier\validator\IsEmailRule;
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use PDO;
|
||||
|
||||
|
||||
|
@ -43,8 +42,6 @@ class SendPasswordResetAction extends Action
|
|||
public function __construct(PDO $conn, UserManager $user_manager, MailManager $mail_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::POST,
|
||||
"send-password-reset",
|
||||
require_valid_csrf_token: true,
|
||||
rule_lists: ["email" => [new IsEmailRule()]],
|
||||
);
|
||||
|
@ -58,27 +55,29 @@ class SendPasswordResetAction extends Action
|
|||
/**
|
||||
* Sends a password reset email.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs `"email": string`: the email address of the account to send a password
|
||||
* reset email for
|
||||
* @return null
|
||||
* @throws ValidationException if another password reset email was sent too recently
|
||||
* @throws InvalidInputException if another password reset email was sent too recently
|
||||
*/
|
||||
function handle(): mixed
|
||||
function handle(array $inputs): mixed
|
||||
{
|
||||
Database::transaction($this->conn, function () {
|
||||
$user_data = $this->user_manager->get_user_by_email($_POST["email"]);
|
||||
Database::transaction($this->conn, function () use ($inputs) {
|
||||
$user_data = $this->user_manager->get_user_by_email($inputs["email"]);
|
||||
|
||||
$minutes_left = Util::minutes_until_interval_elapsed(
|
||||
$user_data["password_reset_token_timestamp"],
|
||||
UserManager::MINUTES_BETWEEN_PASSWORD_RESETS
|
||||
);
|
||||
if ($minutes_left > 0) {
|
||||
throw new ValidationException(
|
||||
throw new InvalidInputException(
|
||||
"A password reset email was sent recently. " .
|
||||
"Please wait $minutes_left more minute(s) before requesting a new email."
|
||||
);
|
||||
}
|
||||
|
||||
$token = $this->user_manager->register_password_reset($_POST["email"]);
|
||||
$this->mail_manager->queue_email(new SendPasswordResetEmail($_POST["email"], $token));
|
||||
$token = $this->user_manager->register_password_reset($inputs["email"]);
|
||||
$this->mail_manager->queue_email(new SendPasswordResetEmail($inputs["email"], $token));
|
||||
});
|
||||
|
||||
return null;
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
namespace com\fwdekker\deathnotifier\user;
|
||||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\validator\IsSetRule;
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use PDO;
|
||||
|
||||
|
||||
|
@ -34,13 +33,12 @@ class ToggleNotificationsAction extends Action
|
|||
public function __construct(PDO $conn, UserManager $user_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::POST,
|
||||
"toggle-notifications",
|
||||
require_logged_in: true,
|
||||
require_valid_csrf_token: true,
|
||||
rule_lists: ["enable_notifications" => [new IsSetRule()]]
|
||||
);
|
||||
|
||||
$this->conn = $conn;
|
||||
$this->user_manager = $user_manager;
|
||||
}
|
||||
|
||||
|
@ -48,17 +46,19 @@ class ToggleNotificationsAction extends Action
|
|||
/**
|
||||
* Sets whether email notifications are sent.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs `"enable_notifications": bool`: `true` if and only if notifications
|
||||
* should be enabled
|
||||
* @return null
|
||||
* @throws ValidationException if the user's email address has not been verified
|
||||
* @throws InvalidInputException if the user's email address has not been verified
|
||||
*/
|
||||
function handle(): mixed
|
||||
function handle(array $inputs): mixed
|
||||
{
|
||||
Database::transaction($this->conn, function() {
|
||||
Database::transaction($this->conn, function() use ($inputs) {
|
||||
$user_data = $this->user_manager->get_user_by_uuid($_SESSION["uuid"]);
|
||||
if ($user_data["email_verification_token"] === null)
|
||||
throw new ValidationException("Please verify your email address before enabling notifications.");
|
||||
throw new InvalidInputException("Please verify your email address before enabling notifications.");
|
||||
|
||||
$this->user_manager->set_notifications_enabled($_SESSION["uuid"], $_POST["enable_notifications"] == true);
|
||||
$this->user_manager->set_notifications_enabled($_SESSION["uuid"], $inputs["enable_notifications"] == true);
|
||||
});
|
||||
|
||||
return null;
|
||||
|
|
|
@ -4,7 +4,6 @@ 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;
|
||||
|
@ -30,8 +29,6 @@ class UserDeleteAction extends Action
|
|||
public function __construct(UserManager $user_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::POST,
|
||||
"user-delete",
|
||||
require_logged_in: true,
|
||||
require_valid_csrf_token: true,
|
||||
rule_lists: [
|
||||
|
@ -49,10 +46,11 @@ class UserDeleteAction extends Action
|
|||
/**
|
||||
* Deletes the currently logged-in user, and terminates the current user session.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs ignored
|
||||
* @return null
|
||||
* @throws ActionException if no CSRF token could be generated
|
||||
*/
|
||||
function handle(): mixed
|
||||
function handle(array $inputs): mixed
|
||||
{
|
||||
$this->user_manager->remove_user_by_uuid($_SESSION["uuid"]);
|
||||
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\user;
|
||||
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\LoggerUtil;
|
||||
use com\fwdekker\deathnotifier\mailer\MailManager;
|
||||
use com\fwdekker\deathnotifier\Response;
|
||||
use Monolog\Logger;
|
||||
use PDO;
|
||||
|
||||
|
@ -49,23 +46,17 @@ class UserManager
|
|||
* @var PDO the database connection to interact with
|
||||
*/
|
||||
private PDO $conn;
|
||||
/**
|
||||
* @var MailManager the mailer to send emails with
|
||||
*/
|
||||
private MailManager $mailer;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new user manager.
|
||||
*
|
||||
* @param PDO $conn the database connection to interact with
|
||||
* @param MailManager $mailer the mailer to send emails with
|
||||
*/
|
||||
public function __construct(PDO $conn, MailManager $mailer)
|
||||
public function __construct(PDO $conn)
|
||||
{
|
||||
$this->logger = LoggerUtil::with_name($this::class);
|
||||
$this->conn = $conn;
|
||||
$this->mailer = $mailer;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
namespace com\fwdekker\deathnotifier\user;
|
||||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\validator\IsEmailRule;
|
||||
use com\fwdekker\deathnotifier\validator\IsSetRule;
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -28,12 +27,10 @@ class ValidatePasswordResetTokenAction extends Action
|
|||
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()],
|
||||
"reset_token" => [new IsSetRule()],
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -44,14 +41,16 @@ class ValidatePasswordResetTokenAction extends Action
|
|||
/**
|
||||
* Checks whether the given password reset token is valid.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs `"email"`: the email address to validate the reset token of,
|
||||
* `"reset_token": string`: the token to validate
|
||||
* @return null
|
||||
* @throws ValidationException if the password reset link is valid
|
||||
* @throws InvalidInputException if the password reset link is valid
|
||||
*/
|
||||
function handle(): mixed
|
||||
function handle(array $inputs): mixed
|
||||
{
|
||||
$user_data = $this->user_manager->get_user_by_email($_GET["email"]);
|
||||
if ($_GET["reset_token"] !== $user_data["password_reset_token"])
|
||||
throw new ValidationException(
|
||||
$user_data = $this->user_manager->get_user_by_email($inputs["email"]);
|
||||
if ($inputs["reset_token"] !== $user_data["password_reset_token"])
|
||||
throw new InvalidInputException(
|
||||
"This password reset link is invalid. Maybe you already reset your password?"
|
||||
);
|
||||
|
||||
|
|
|
@ -3,13 +3,12 @@
|
|||
namespace com\fwdekker\deathnotifier\user;
|
||||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\mailer\MailManager;
|
||||
use com\fwdekker\deathnotifier\Util;
|
||||
use com\fwdekker\deathnotifier\validator\IsEmailRule;
|
||||
use com\fwdekker\deathnotifier\validator\IsSetRule;
|
||||
use com\fwdekker\deathnotifier\validator\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use PDO;
|
||||
|
||||
|
||||
|
@ -42,8 +41,6 @@ class VerifyEmailAction extends Action
|
|||
public function __construct(PDO $conn, UserManager $user_manager, MailManager $mail_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::POST,
|
||||
"verify-email",
|
||||
require_valid_csrf_token: true,
|
||||
rule_lists: [
|
||||
"email" => [new IsEmailRule()],
|
||||
|
@ -60,17 +57,19 @@ class VerifyEmailAction extends Action
|
|||
/**
|
||||
* Verifies the user's email address.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs `"email"`: the email address to verify, `"verify_token": string`: the
|
||||
* token to verify the email address with
|
||||
* @return null
|
||||
* @throws ValidationException if the email address does not exist, if the email is already verified, or if the
|
||||
* @throws InvalidInputException if the email address does not exist, if the email is already verified, or if the
|
||||
* token expired
|
||||
*/
|
||||
function handle(): mixed
|
||||
function handle(array $inputs): mixed
|
||||
{
|
||||
Database::transaction($this->conn, function () {
|
||||
Database::transaction($this->conn, function () use ($inputs) {
|
||||
// TODO: Validate that the email even exists
|
||||
$user_data = $this->user_manager->get_user_by_email($_POST["email"]);
|
||||
if ($user_data["email_verification_token"] !== $_POST["verify_token"])
|
||||
throw new ValidationException(
|
||||
$user_data = $this->user_manager->get_user_by_email($inputs["email"]);
|
||||
if ($user_data["email_verification_token"] !== $inputs["verify_token"])
|
||||
throw new InvalidInputException(
|
||||
"Failed to verify email address. Maybe you already verified your email address?"
|
||||
);
|
||||
|
||||
|
@ -79,7 +78,7 @@ class VerifyEmailAction extends Action
|
|||
UserManager::MINUTES_VALID_VERIFICATION
|
||||
);
|
||||
if ($minutes_remaining < 0)
|
||||
throw new ValidationException(
|
||||
throw new InvalidInputException(
|
||||
"This email verification link has expired. Log in and request a new verification email."
|
||||
);
|
||||
|
||||
|
|
|
@ -41,20 +41,20 @@ class HasLengthRule extends Rule
|
|||
* @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
|
||||
* @throws InvalidInputException if `$inputs[$key]` is not set or is not of the specified length
|
||||
*/
|
||||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!isset($inputs[$key]))
|
||||
throw new ValidationException($this->override_message ?? "Missing input '$key'.", $key);
|
||||
throw new InvalidInputException($this->override_message ?? "Missing input '$key'.", $key);
|
||||
|
||||
if ($this->min_length !== null && strlen($inputs[$key]) < $this->min_length)
|
||||
throw new ValidationException(
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ?? "Use at least $this->min_length character(s).",
|
||||
$key
|
||||
);
|
||||
if ($this->max_length !== null && strlen($inputs[$key]) > $this->max_length)
|
||||
throw new ValidationException(
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ?? "Use at most $this->max_length character(s).",
|
||||
$key
|
||||
);
|
||||
|
|
|
@ -2,13 +2,16 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
use Exception;
|
||||
use com\fwdekker\deathnotifier\MalformedRequestException;
|
||||
use Throwable;
|
||||
|
||||
|
||||
/**
|
||||
* Thrown if an action could not be handled because an input is invalid.
|
||||
* Thrown if a client-specified input is invalid.
|
||||
*
|
||||
* This is an exception, not an error, so it indicates that the client that sent the request did something wrong.
|
||||
*/
|
||||
class ValidationException extends Exception
|
||||
class InvalidInputException extends MalformedRequestException
|
||||
{
|
||||
/**
|
||||
* @var string|null the input element that caused the exception, or `null` if no such element could be identified
|
||||
|
@ -22,10 +25,11 @@ class ValidationException extends Exception
|
|||
* @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
|
||||
* @param Throwable|null $previous the throwable that caused this one
|
||||
*/
|
||||
public function __construct(string $message, ?string $target = null)
|
||||
public function __construct(string $message, ?string $target = null, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message);
|
||||
parent::__construct($message, previous: $previous);
|
||||
|
||||
$this->target = $target;
|
||||
}
|
|
@ -14,11 +14,11 @@ class IsEmailRule extends Rule
|
|||
* @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
|
||||
* @throws InvalidInputException if `$inputs[$key]` is not set or is not an email address
|
||||
*/
|
||||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!isset($inputs[$key]) || !filter_var($inputs[$key], FILTER_VALIDATE_EMAIL))
|
||||
throw new ValidationException($this->override_message ?? "Enter a valid email address.", $key);
|
||||
throw new InvalidInputException($this->override_message ?? "Enter a valid email address.", $key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,12 +33,12 @@ class IsEqualToRule extends Rule
|
|||
* @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`
|
||||
* @throws InvalidInputException 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(
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ?? "Inputs '$key' should equal '$this->expected'.",
|
||||
$key
|
||||
);
|
||||
|
|
|
@ -14,11 +14,11 @@ class IsNotBlankRule extends Rule
|
|||
* @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
|
||||
* @throws InvalidInputException if `$inputs[$key]` is not set or if `trim($inputs[$key])` is an empty string
|
||||
*/
|
||||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!isset($inputs[$key]) || trim($inputs[$key]) === "")
|
||||
throw new ValidationException($this->override_message ?? "Use at least one character.", $key);
|
||||
throw new InvalidInputException($this->override_message ?? "Use at least one character.", $key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,12 +14,12 @@ class IsNotSetRule extends Rule
|
|||
* @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
|
||||
* @throws InvalidInputException if `$inputs[$key]` is set
|
||||
*/
|
||||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (isset($inputs[$key]))
|
||||
throw new ValidationException(
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ?? "Field '" . htmlentities($key) . "' must not be set.",
|
||||
$key
|
||||
);
|
||||
|
|
|
@ -14,12 +14,12 @@ class IsSetRule extends Rule
|
|||
* @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
|
||||
* @throws InvalidInputException if `$inputs[$key]` is not set
|
||||
*/
|
||||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!isset($inputs[$key]))
|
||||
throw new ValidationException(
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ?? "Field '" . htmlentities($key) . "' must be set.",
|
||||
$key
|
||||
);
|
||||
|
|
|
@ -35,7 +35,7 @@ abstract class Rule
|
|||
* @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
|
||||
* @throws InvalidInputException if the rule does not hold
|
||||
*/
|
||||
public abstract function check(array $inputs, string $key): void;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue