death-notifier/src/main/php/com/fwdekker/deathnotifier/user/ChangeEmailAction.php

207 lines
6.9 KiB
PHP

<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\mailer\Email;
use com\fwdekker\deathnotifier\mailer\EmailQueue;
use com\fwdekker\deathnotifier\tracking\TrackingList;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsEmailRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
/**
* Changes the user's email address and sends the user a verification link.
*
* @see ChangeEmailFromEmail
* @see ChangeEmailToEmail
*/
class ChangeEmailAction extends Action
{
/**
* @var UserList the list to change the user's email address in
*/
private readonly UserList $user_list;
/**
* @var EmailQueue the queue to add notifications to
*/
private readonly EmailQueue $email_queue;
/**
* Constructs a new `ChangeEmailAction`.
*
* @param UserList $user_list the list to change the user's email address in
* @param EmailQueue $email_queue the queue to add notifications to
*/
public function __construct(UserList $user_list, EmailQueue $email_queue)
{
$this->user_list = $user_list;
$this->email_queue = $email_queue;
}
/**
* Changes the user's email address and sends the user a verification link.
*
* Requires that the user is logged in and that a valid CSRF token is present.
*
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token, `"email": string`: the new email
* address
* @return null
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is not logged in, if no valid CSRF token is present, if the email
* address is invalid, if the email address is not different, or if the email address is already used
* @throws UnexpectedException if the current user has been deleted
* @noinspection PhpDocRedundantThrowsInspection can be thrown through {@see TrackingList::transaction()}
*/
public function handle(array $inputs): mixed
{
(new LoginValidator(validate_logged_in: true))->check($_SESSION);
(new RuleSet([
"token" => [new IsValidCsrfTokenRule()],
"email" => [new IsEmailRule()],
]))->check($inputs);
$this->user_list->transaction(function () use ($inputs) {
$user_data = $this->user_list->get_user_by_uuid($_SESSION["uuid"]);
if ($user_data === null)
throw new UnexpectedException("Failed to retrieve user data. Please refresh the page and try again.");
if ($inputs["email"] === $user_data["email"])
throw new InvalidValueException("That is already your email address.", "email");
if ($this->user_list->has_user_with_email($inputs["email"]))
throw new InvalidValueException("That email address is already in use by someone else.", "email");
$token = $this->user_list->set_email($_SESSION["uuid"], $inputs["email"]);
$this->email_queue->enqueue([
new ChangeEmailFromEmail($user_data["email"], $inputs["email"]),
new ChangeEmailToEmail($inputs["email"], $user_data["email"], $token)
]);
});
return null;
}
}
/**
* An email informing a user that their email has been changed and needs to be verified, sent to the user's old email
* address.
*
* @see ChangeEmailAction
*/
class ChangeEmailFromEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "changed-email-from";
/**
* @var string the new email address
*/
public readonly string $new_email;
/**
* Constructs a new `ChangeEmailFromEmail`.
*
* @param string $recipient the intended recipient of this email
* @param string $new_email the new email address
*/
public function __construct(string $recipient, string $new_email)
{
parent::__construct($this::TYPE, $recipient);
$this->new_email = $new_email;
}
public function get_subject(): string
{
return "Your email address has been changed";
}
public function get_body(): string
{
$base_path = Config::get("server.base_path");
return
"You changed the email address of your Death Notifier account from $this->recipient to $this->new_email. " .
"Until you verify your email address, you will not receive any notifications on either email address. " .
"Check the inbox of $this->new_email for a verification link." .
"\n\n" .
"If you did not request a change of email address, log in at the Death Notifier website, change your " .
"password, and change your email address back." .
"\n\n" .
$base_path;
}
}
/**
* An email informing a user that their email has been changed and needs to be verified, sent to the user's new email
* address that needs to be verified.
*
* @see ChangeEmailAction
*/
class ChangeEmailToEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "changed-email-to";
/**
* @var string the old email address
*/
public readonly string $old_email;
/**
* @var string the token to verify the email address with
*/
public readonly string $token;
/**
* Constructs a new `ChangeEmailEmail`.
*
* @param string $recipient the intended recipient of this email
* @param string $old_email the old email address
* @param string $token the token to verify the email address with
*/
public function __construct(string $recipient, string $old_email, string $token)
{
parent::__construct($this::TYPE, $recipient);
$this->old_email = $old_email;
$this->token = $token;
}
public function get_subject(): string
{
return "Verify your new email address";
}
public function get_body(): string
{
$base_path = Config::get("server.base_path");
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
return
"You changed the email address of your Death Notifier account from $this->old_email to $this->recipient. " .
"Until you verify your email address, you will not receive any notifications. " .
"You can verify your new email address by clicking the link below. " .
"This link will expire after " . UserList::MINUTES_VALID_VERIFICATION . " minutes." .
"\n" .
"Verify: $verify_path" .
"\n\n" .
$base_path;
}
}