Restructure all files and classes

This is the *only* way in which I could get the autoloader to work for both my production and my test code :(
This commit is contained in:
Florine W. Dekker 2022-11-27 14:59:00 +01:00
parent 379498cfe0
commit 43feaa0fed
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
36 changed files with 1044 additions and 936 deletions

View File

@ -17,7 +17,13 @@ module.exports = grunt => {
files: [{expand: true, cwd: "src/main/", src: "**/*.html", dest: "dist/", flatten: true}]
},
php: {
files: [{expand: true, cwd: "src/main/", src: "**/*.php", dest: "dist/", flatten: false}]
files: [
{expand: true, cwd: "src/main/", src: "*.php", dest: "dist/", flatten: false},
{expand: true, cwd: "src/main/php/", src: "**/*.php", dest: "dist/", flatten: false},
]
},
php_test: {
files: [{expand: true, cwd: "src/test/php", src: "**/*.php", dest: "dist/", flatten: false}]
},
},
focus: {
@ -63,7 +69,7 @@ module.exports = grunt => {
exec: "composer.phar install --no-dev"
},
phpunit: {
exec: "vendor/bin/phpunit --testdox src/test"
exec: "cd dist/ && chmod +x .vendor/bin/phpunit && .vendor/bin/phpunit --testdox ."
},
stan: {
exec: "vendor/bin/phpstan analyse -l 8 src/main src/test"
@ -79,8 +85,8 @@ module.exports = grunt => {
tasks: ["copy:html", "replace:dev"],
},
php: {
files: ["src/main/**/*.php"],
tasks: ["copy:php", "replace:dev"],
files: ["src/**/*.php"],
tasks: ["copy:php", "copy:php_test", "replace:dev"],
},
ts: {
files: ["src/main/**/*.ts"],
@ -135,6 +141,7 @@ module.exports = grunt => {
"copy:css",
"copy:html",
"copy:php",
"copy:php_test",
// Compile JS
"webpack:dev",
"replace:dev",

View File

@ -29,7 +29,7 @@
},
"autoload": {
"psr-4": {
"php\\": "php/"
"com\\fwdekker\\deathnotifier\\": "com/fwdekker/deathnotifier/"
}
},
"minimum-stability": "stable"

View File

@ -1,17 +1,17 @@
<?php
use php\Database;
use php\IsEmailRule;
use php\IsNotBlankRule;
use php\IsSetRule;
use php\LengthRule;
use php\Mailer;
use php\Mediawiki;
use php\Response;
use php\TrackingManager;
use php\UserManager;
use php\Util;
use php\Validator;
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\trackings\TrackingManager;
use com\fwdekker\deathnotifier\UserManager;
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";

View File

@ -1,653 +0,0 @@
<?php
namespace php;
use Exception;
use Monolog\Logger;
use PDO;
use PHPMailer\PHPMailer\Exception as MailerException;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
/**
* Queues up mails and sends them when appropriate.
*/
class Mailer
{
/**
* @var Logger The logger to use for logging.
*/
private Logger $logger;
/**
* @var array<string, array<string, mixed>> The configuration to use for mailing.
*/
private array $config;
/**
* @var PDO The database connection to interact with.
*/
private PDO $conn;
/**
* Constructs a new mailer.
*
* @param array<string, array<string, mixed>> $config the configuration to use for mailing
* @param Logger $logger the logger to use for logging
* @param PDO $conn the connection to the email database
*/
public function __construct(array $config, Logger $logger, PDO $conn)
{
$this->logger = $logger;
$this->config = $config;
$this->conn = $conn;
}
/**
* Populates the database with the necessary structures for emails.
*
* @return void
*/
public function install(): void
{
$this->conn->exec("CREATE TABLE email_tasks(type TEXT NOT NULL,
recipient TEXT NOT NULL,
arg1 TEXT NOT NULL DEFAULT(''),
arg2 TEXT NOT NULL DEFAULT(''),
PRIMARY KEY (type, recipient, arg1, arg2));");
}
/**
* Queues an email to be sent.
*
* @param Email $email the email to queue
* @return Response a satisfied `Response` if the email was queued, or an unsatisfied `Response` otherwise
*/
public function queue_email(Email $email): Response
{
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO email_tasks (type, recipient, arg1, arg2)
VALUES (:type, :recipient, :arg1, :arg2);");
$stmt->bindValue(":type", $email->type);
$stmt->bindValue(":recipient", $email->recipient);
$stmt->bindValue(":arg1", $email->arg1);
$stmt->bindValue(":arg2", $email->arg2);
return $stmt->execute() ? Response::satisfied() : Response::unsatisfied(null);
}
/**
* Sends all emails in the queue.
*
* @return void
*/
public function process_queue(): void
{
// Open mailer
$mailer = new PHPMailer();
$mailer->IsSMTP();
$mailer->CharSet = "UTF-8";
$mailer->SMTPAuth = true;
$mailer->SMTPDebug = SMTP::DEBUG_OFF;
$mailer->SMTPKeepAlive = true;
$mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$mailer->Host = $this->config["mail"]["host"];
$mailer->Port = $this->config["mail"]["port"];
$mailer->Username = $this->config["mail"]["username"];
$mailer->Password = $this->config["mail"]["password"];
try {
$mailer->setFrom($this->config["mail"]["username"], $this->config["mail"]["from_name"]);
} catch (MailerException $exception) {
$this->logger->error("Failed to set 'from' address while processing queue.", ["cause" => $exception]);
$mailer->smtpClose();
}
// Get queue
$stmt = $this->conn->prepare("SELECT type, recipient, arg1, arg2 FROM email_tasks;");
$stmt->execute();
$email_tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Process queue
$stmt = $this->conn->prepare("DELETE FROM email_tasks
WHERE type=:type AND recipient=:recipient AND arg1=:arg1 AND arg2=:arg2;");
$stmt->bindParam(":type", $type);
$stmt->bindParam(":recipient", $recipient);
$stmt->bindParam(":arg1", $arg1);
$stmt->bindParam(":arg2", $arg2);
foreach ($email_tasks as ["type" => $type, "recipient" => $recipient, "arg1" => $arg1, "arg2" => $arg2]) {
try {
$email = Email::deserialize($type, $recipient, $arg1, $arg2);
$mailer->Subject = $email->getSubject();
$mailer->Body = $email->getBody($this->config);
try {
$mailer->addAddress($recipient);
$mailer->send();
} catch (MailerException $exception) {
$this->logger->error(
"Failed to send mail.",
["cause" => $exception, "email" => $email]
);
$mailer->getSMTPInstance()->reset();
}
$mailer->clearAddresses();
} catch (Exception $exception) {
$this->logger->error(
"Failed to send mail.",
["cause" => $exception]
);
$mailer->getSMTPInstance()->reset();
}
$stmt->execute();
}
}
}
/**
* A serializable email that can be queued in a database and can be sent.
*
* When serialized, an email is represented by the type of email and two arguments. When deserialized, an email can be
* constructed from those arguments, and the implementation returns a specific subject and message.
*/
abstract class Email
{
/**
* @var string A string identifying the type of email.
*/
public string $type;
/**
* @var string The intended recipient of the email.
*/
public string $recipient;
/**
* @var string The first argument to construct the email.
*/
public string $arg1 = "";
/**
* @var string The second argument to construct the email.
*/
public string $arg2 = "";
/**
* Returns the subject header of the email.
*
* @return string the subject header of the email
*/
public abstract function getSubject(): string;
/**
* Returns the body of the email.
*
* @param array<string, array<string, mixed>> $config the software configuration
* @return string the body of the email
*/
public abstract function getBody(array $config): string;
/**
* Deserializes an email back into an instance.
*
* Only types of emails that are known can be deserialized.
*
* @param string $type the type of email to deserialize
* @param string $recipient the intended recipient of the email
* @param string $arg1 the first argument to construct the email
* @param string $arg2 the second argument to construct the email
* @return Email a deserialized email
* @throws Exception if the `type` is not recognized
*/
public static function deserialize(string $type, string $recipient, string $arg1, string $arg2): Email
{
return match ($type) {
RegisterEmail::TYPE => new RegisterEmail($recipient, $arg1),
VerifyEmailEmail::TYPE => new VerifyEmailEmail($recipient, $arg1),
ChangedEmailEmail::TYPE => new ChangedEmailEmail($recipient, $arg1),
ChangedPasswordEmail::TYPE => new ChangedPasswordEmail($recipient),
ResetPasswordEmail::TYPE => new ResetPasswordEmail($recipient, $arg1),
NotifyArticleDeletedEmail::TYPE => new NotifyArticleDeletedEmail($recipient, $arg1),
NotifyArticleUndeletedEmail::TYPE => new NotifyArticleUndeletedEmail($recipient, $arg1),
NotifyStatusChangedEmail::TYPE => new NotifyStatusChangedEmail($recipient, $arg1, $arg2),
default => throw new Exception("Unknown email type $type."),
};
}
}
/**
* An email to be sent to a recently registered user, including instructions for email verification.
*/
class RegisterEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "register";
/**
* @var string The token to verify the email address with.
*/
public string $token;
/**
* Constructs an email to be sent to a recently registered user, including instructions for email verification.
*
* @param string $recipient the intended recipient of the email
* @param string $token the token to verify the email address with
*/
public function __construct(string $recipient, string $token)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->token = $token;
$this->arg1 = $token;
}
public function getSubject(): string
{
return "Welcome to Death Notifier";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
return
"You have successfully created an account with Death Notifier. " .
"Welcome!" .
"\n\n" .
"Until you verify your email address, you will not receive any notifications. " .
"You can verify your email address by clicking the link below. " .
"This link will expire after " . UserManager::MINUTES_VALID_VERIFICATION . " minutes." .
"\n" .
"Verify: $verify_path" .
"\n\n" .
"If you did not create this account, you can delete the account. " .
"Go to the Death Notifier website, reset your password, log in, and delete the account." .
"\n\n" .
$base_path;
}
}
/**
* An email to help a user verify their email address.
*/
class VerifyEmailEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "verify-email";
/**
* @var string The token to verify the email address with.
*/
public string $token;
/**
* Constructs an email to help a user verify their email address.
*
* @param string $recipient the intended recipient of the email
* @param string $token the token to verify the email address with
*/
public function __construct(string $recipient, string $token)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->token = $token;
$this->arg1 = $token;
}
public function getSubject(): string
{
return "Verify your email address";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
return
"You requested a new verification link for your Death Notifier account. " .
"You can verify your email address by clicking the link below. " .
"This link will expire after " . UserManager::MINUTES_VALID_VERIFICATION . " minutes. " .
"Until you verify your email address, you will not receive any notifications." .
"\n" .
"Verify: $verify_path" .
"\n\n" .
$base_path;
}
}
/**
* An email informing a user that their email has been changed, and needs verification.
*/
class ChangedEmailEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "changed-email";
/**
* @var string The token to verify the email address with.
*/
public string $token;
/**
* Constructs an email informing a user that their email has been changed, and needs verification.
*
* @param string $recipient the intended recipient of the email
* @param string $token the token to verify the email address with
*/
public function __construct(string $recipient, string $token)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->token = $token;
$this->arg1 = $token;
}
public function getSubject(): string
{
return "Verify your new email address";
}
public function getBody(array $config): string
{
$base_path = $config["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. " .
"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 " . UserManager::MINUTES_VALID_VERIFICATION . " minutes." .
"\n" .
"Verify: $verify_path" .
"\n\n" .
$base_path;
}
}
/**
* An email informing a user that their password has been changed.
*/
class ChangedPasswordEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "changed-password";
/**
* Constructs an email informing a user that their email has been changed, and needs verification.
*
* @param string $recipient the intended recipient of the email
*/
public function __construct(string $recipient)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
}
public function getSubject(): string
{
return "Your password has been changed";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
return
"You changed the password of your Death Notifier account." .
"\n\n" .
"If you did not change the password of your account, go to the Death Notifier website and use the forgot " .
"password option to change your password back." .
"\n\n" .
$base_path;
}
}
/**
* An email to help a user reset their password.
*/
class ResetPasswordEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "reset-password";
/**
* @var string The token to reset the password with.
*/
public string $token;
/**
* Constructs an email to help a user reset their password.
*
* @param string $recipient the intended recipient of the email
* @param string $token the token to reset the password with
*/
public function __construct(string $recipient, string $token)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->token = $token;
$this->arg1 = $token;
}
public function getSubject(): string
{
return "Reset your password";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
$verify_path =
"$base_path?action=reset-password&email=" . rawurlencode($this->recipient) . "&token=$this->token";
return
"You requested a password reset link for your Death Notifier account. " .
"You can choose a new password by clicking the link below. " .
"This link expires after " . UserManager::MINUTES_VALID_PASSWORD_RESET . " minutes." .
"\n" .
"Reset password: $verify_path" .
"\n\n" .
"If you did not request a new password, you can safely ignore this message." .
"\n\n" .
$base_path;
}
}
/**
* An email to inform a user that a tracked article has been deleted.
*/
class NotifyArticleDeletedEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "notify-article-deleted";
/**
* @var string The name of the article that was deleted.
*/
public string $name;
/**
* Constructs an email to inform a user that a tracked article has been deleted.
*
* @param string $recipient the intended recipient of the email
* @param string $name the name of the article that was deleted
*/
public function __construct(string $recipient, string $name)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->name = $name;
$this->arg1 = $name;
}
public function getSubject(): string
{
return "$this->name article has been deleted";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
return
"The Wikipedia article about $this->name has been deleted. " .
"Death Notifier is now unable to send you a notification if $this->name dies. " .
"If the Wikipedia article is ever re-created, Death Notifier will automatically resume tracking this " .
"article, and you will receive another notification." .
"\n\n" .
"You are receiving this message because of the preferences in your Death Notifier account. " .
"To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " .
"preferences." .
"\n\n" .
$base_path;
}
}
/**
* An email to inform a user that a tracked article has been re-created.
*/
class NotifyArticleUndeletedEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "notify-article-undeleted";
/**
* @var string The name of the article that was re-created.
*/
public string $name;
/**
* Constructs an email to inform a user that a tracked article has been re-created.
*
* @param string $recipient the intended recipient of the email
* @param string $name the name of the article that was re-created
*/
public function __construct(string $recipient, string $name)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->name = $name;
$this->arg1 = $name;
}
public function getSubject(): string
{
return "$this->name article has been re-created";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
return
"The Wikipedia article about $this->name has been re-created. " .
"Death Notifier will once again track the article and notify you if $this->name dies." .
"\n\n" .
"You are receiving this message because of the preferences in your Death Notifier account. " .
"To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " .
"preferences." .
"\n\n" .
$base_path;
}
}
/**
* An email to inform a user a tracker person's status has changed.
*/
class NotifyStatusChangedEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "notify-status-changed";
/**
* @var string The name of the person whose status has changed.
*/
public string $name;
/**
* @var string The new status of the person.
*/
public string $new_status;
/**
* Constructs an email to inform a user someone has died.
*
* @param string $recipient the intended recipient of the email
* @param string $name the name of the person who died
* @param string $new_status the new status of the person
*/
public function __construct(string $recipient, string $name, string $new_status)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->name = $name;
$this->arg1 = $name;
$this->new_status = $new_status;
$this->arg2 = $new_status;
}
public function getSubject(): string
{
return "$this->name may be $this->new_status";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
return
"Someone has edited Wikipedia to state that $this->name is $this->new_status. " .
"For more information, read their Wikipedia article at " .
"https://en.wikipedia.org/wiki/" . rawurlencode($this->name) .
"\n\n" .
"You are receiving this message because of the preferences in your Death Notifier account. " .
"To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " .
"preferences." .
"\n\n" .
$base_path;
}
}

View File

@ -1,233 +0,0 @@
<?php
namespace php;
/**
* 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;
}
}
/**
* A rule/constraint/assertion that should hold over an input.
*/
abstract class Rule
{
/**
* @var string|null The message to return if the rule does not apply to some input. If `null`, the rule
* implementation can choose an appropriate message.
*/
public ?string $override_message;
/**
* Instantiates a new rule.
*
* @param string|null $override_message the message to return if the rule does not apply to some input. If `null`,
* the rule implementation can choose an appropriate message
*/
public function __construct(?string $override_message = null)
{
$this->override_message = $override_message;
}
/**
* Checks whether the rule holds for `$inputs[$key]`.
*
* Implementations should never assume that the `$inputs[$key]` is set.
*
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param string $key the key in `inputs` of the input to check
* @return Response|null `null` if the rule holds, or an unsatisfied `Response` otherwise
*/
public abstract function check(array $inputs, string $key): ?Response;
}
/**
* Requires the input to be a valid email address.
*/
class IsEmailRule extends Rule
{
/**
* Checks whether the input is a valid 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
*/
public function check(array $inputs, string $key): ?Response
{
return !isset($inputs[$key]) || !filter_var($inputs[$key], FILTER_VALIDATE_EMAIL)
? Response::unsatisfied($this->override_message ?? "Enter a valid email address.", $key)
: null;
}
}
/**
* Requires the input to not be blank.
*/
class IsNotBlankRule extends Rule
{
/**
* Checks whether 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
*/
public function check(array $inputs, string $key): ?Response
{
return !isset($inputs[$key]) || trim($inputs[$key]) === ""
? Response::unsatisfied($this->override_message ?? "Use at least one character.", $key)
: null;
}
}
/**
* Requires the input to be set.
*/
class IsSetRule extends Rule
{
/**
* Checks whether 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
*/
public function check(array $inputs, string $key): ?Response
{
return !isset($inputs[$key])
? Response::unsatisfied($this->override_message ?? "Field '" . htmlentities($key) . "' required.", $key)
: null;
}
}
/**
* Requires the input to be of a specific length.
*/
class LengthRule extends Rule
{
/**
* @var int|null The minimum length (inclusive), or `null` if there is no minimum length.
*/
private readonly ?int $min_length;
/**
* @var int|null The maximum length (inclusive), or `null` if there is no maximum length.
*/
private readonly ?int $max_length;
/**
* Instantiates a new rule.
*
* @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
* @param string|null $override_message the message to return if the rule does not apply to some input. If `null`,
* the rule implementation can choose an appropriate message
*/
public function __construct(?int $min_length = null, ?int $max_length = null, ?string $override_message = null)
{
parent::__construct($override_message);
$this->min_length = $min_length;
$this->max_length = $max_length;
}
/**
* Checks whether the input is of the specified 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
*/
public function check(array $inputs, string $key): ?Response
{
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(
$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(
$this->override_message ?? "Use at most $this->max_length character(s).",
$key
);
else
return null;
}
}

View File

@ -1,7 +1,9 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\mailer\Mailer;
use com\fwdekker\deathnotifier\trackings\TrackingManager;
use Composer\Semver\Comparator;
use Monolog\Logger;
use PDO;

View File

@ -1,7 +1,8 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\trackings\PersonStatus;
use Exception;
use Monolog\Logger;

View File

@ -1,6 +1,6 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier;
/**

View File

@ -1,7 +1,13 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\mailer\ChangedEmailEmail;
use com\fwdekker\deathnotifier\mailer\ChangedPasswordEmail;
use com\fwdekker\deathnotifier\mailer\Mailer;
use com\fwdekker\deathnotifier\mailer\RegisterEmail;
use com\fwdekker\deathnotifier\mailer\ResetPasswordEmail;
use com\fwdekker\deathnotifier\mailer\VerifyEmailEmail;
use DateTime;
use Monolog\Logger;
use PDO;

View File

@ -1,6 +1,6 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier;
use Exception;
use Monolog\ErrorHandler;

View File

@ -0,0 +1,60 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
use com\fwdekker\deathnotifier\UserManager;
/**
* An email informing a user that their email has been changed, and needs verification.
*/
class ChangedEmailEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "changed-email";
/**
* @var string The token to verify the email address with.
*/
public string $token;
/**
* Constructs an email informing a user that their email has been changed, and needs verification.
*
* @param string $recipient the intended recipient of the email
* @param string $token the token to verify the email address with
*/
public function __construct(string $recipient, string $token)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->token = $token;
$this->arg1 = $token;
}
public function getSubject(): string
{
return "Verify your new email address";
}
public function getBody(array $config): string
{
$base_path = $config["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. " .
"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 " . UserManager::MINUTES_VALID_VERIFICATION . " minutes." .
"\n" .
"Verify: $verify_path" .
"\n\n" .
$base_path;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
/**
* An email informing a user that their password has been changed.
*/
class ChangedPasswordEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "changed-password";
/**
* Constructs an email informing a user that their email has been changed, and needs verification.
*
* @param string $recipient the intended recipient of the email
*/
public function __construct(string $recipient)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
}
public function getSubject(): string
{
return "Your password has been changed";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
return
"You changed the password of your Death Notifier account." .
"\n\n" .
"If you did not change the password of your account, go to the Death Notifier website and use the forgot " .
"password option to change your password back." .
"\n\n" .
$base_path;
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
use Exception;
/**
* A serializable email that can be queued in a database and can be sent.
*
* When serialized, an email is represented by the type of email and two arguments. When deserialized, an email can be
* constructed from those arguments, and the implementation returns a specific subject and message.
*/
abstract class Email
{
/**
* @var string A string identifying the type of email.
*/
public string $type;
/**
* @var string The intended recipient of the email.
*/
public string $recipient;
/**
* @var string The first argument to construct the email.
*/
public string $arg1 = "";
/**
* @var string The second argument to construct the email.
*/
public string $arg2 = "";
/**
* Returns the subject header of the email.
*
* @return string the subject header of the email
*/
public abstract function getSubject(): string;
/**
* Returns the body of the email.
*
* @param array<string, array<string, mixed>> $config the software configuration
* @return string the body of the email
*/
public abstract function getBody(array $config): string;
/**
* Deserializes an email back into an instance.
*
* Only types of emails that are known can be deserialized.
*
* @param string $type the type of email to deserialize
* @param string $recipient the intended recipient of the email
* @param string $arg1 the first argument to construct the email
* @param string $arg2 the second argument to construct the email
* @return Email a deserialized email
* @throws Exception if the `type` is not recognized
*/
public static function deserialize(string $type, string $recipient, string $arg1, string $arg2): Email
{
return match ($type) {
RegisterEmail::TYPE => new RegisterEmail($recipient, $arg1),
VerifyEmailEmail::TYPE => new VerifyEmailEmail($recipient, $arg1),
ChangedEmailEmail::TYPE => new ChangedEmailEmail($recipient, $arg1),
ChangedPasswordEmail::TYPE => new ChangedPasswordEmail($recipient),
ResetPasswordEmail::TYPE => new ResetPasswordEmail($recipient, $arg1),
NotifyArticleDeletedEmail::TYPE => new NotifyArticleDeletedEmail($recipient, $arg1),
NotifyArticleUndeletedEmail::TYPE => new NotifyArticleUndeletedEmail($recipient, $arg1),
NotifyStatusChangedEmail::TYPE => new NotifyStatusChangedEmail($recipient, $arg1, $arg2),
default => throw new Exception("Unknown email type $type."),
};
}
}

View File

@ -0,0 +1,149 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
use com\fwdekker\deathnotifier\Response;
use Exception;
use Monolog\Logger;
use PDO;
use PHPMailer\PHPMailer\Exception as MailerException;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
/**
* Queues up mails and sends them when appropriate.
*/
class Mailer
{
/**
* @var Logger The logger to use for logging.
*/
private Logger $logger;
/**
* @var array<string, array<string, mixed>> The configuration to use for mailing.
*/
private array $config;
/**
* @var PDO The database connection to interact with.
*/
private PDO $conn;
/**
* Constructs a new mailer.
*
* @param array<string, array<string, mixed>> $config the configuration to use for mailing
* @param Logger $logger the logger to use for logging
* @param PDO $conn the connection to the email database
*/
public function __construct(array $config, Logger $logger, PDO $conn)
{
$this->logger = $logger;
$this->config = $config;
$this->conn = $conn;
}
/**
* Populates the database with the necessary structures for emails.
*
* @return void
*/
public function install(): void
{
$this->conn->exec("CREATE TABLE email_tasks(type TEXT NOT NULL,
recipient TEXT NOT NULL,
arg1 TEXT NOT NULL DEFAULT(''),
arg2 TEXT NOT NULL DEFAULT(''),
PRIMARY KEY (type, recipient, arg1, arg2));");
}
/**
* Queues an email to be sent.
*
* @param Email $email the email to queue
* @return Response a satisfied `Response` if the email was queued, or an unsatisfied `Response` otherwise
*/
public function queue_email(Email $email): Response
{
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO email_tasks (type, recipient, arg1, arg2)
VALUES (:type, :recipient, :arg1, :arg2);");
$stmt->bindValue(":type", $email->type);
$stmt->bindValue(":recipient", $email->recipient);
$stmt->bindValue(":arg1", $email->arg1);
$stmt->bindValue(":arg2", $email->arg2);
return $stmt->execute() ? Response::satisfied() : Response::unsatisfied(null);
}
/**
* Sends all emails in the queue.
*
* @return void
*/
public function process_queue(): void
{
// Open mailer
$mailer = new PHPMailer();
$mailer->IsSMTP();
$mailer->CharSet = "UTF-8";
$mailer->SMTPAuth = true;
$mailer->SMTPDebug = SMTP::DEBUG_OFF;
$mailer->SMTPKeepAlive = true;
$mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$mailer->Host = $this->config["mail"]["host"];
$mailer->Port = $this->config["mail"]["port"];
$mailer->Username = $this->config["mail"]["username"];
$mailer->Password = $this->config["mail"]["password"];
try {
$mailer->setFrom($this->config["mail"]["username"], $this->config["mail"]["from_name"]);
} catch (MailerException $exception) {
$this->logger->error("Failed to set 'from' address while processing queue.", ["cause" => $exception]);
$mailer->smtpClose();
}
// Get queue
$stmt = $this->conn->prepare("SELECT type, recipient, arg1, arg2 FROM email_tasks;");
$stmt->execute();
$email_tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Process queue
$stmt = $this->conn->prepare("DELETE FROM email_tasks
WHERE type=:type AND recipient=:recipient AND arg1=:arg1 AND arg2=:arg2;");
$stmt->bindParam(":type", $type);
$stmt->bindParam(":recipient", $recipient);
$stmt->bindParam(":arg1", $arg1);
$stmt->bindParam(":arg2", $arg2);
foreach ($email_tasks as ["type" => $type, "recipient" => $recipient, "arg1" => $arg1, "arg2" => $arg2]) {
try {
$email = Email::deserialize($type, $recipient, $arg1, $arg2);
$mailer->Subject = $email->getSubject();
$mailer->Body = $email->getBody($this->config);
try {
$mailer->addAddress($recipient);
$mailer->send();
} catch (MailerException $exception) {
$this->logger->error(
"Failed to send mail.",
["cause" => $exception, "email" => $email]
);
$mailer->getSMTPInstance()->reset();
}
$mailer->clearAddresses();
} catch (Exception $exception) {
$this->logger->error(
"Failed to send mail.",
["cause" => $exception]
);
$mailer->getSMTPInstance()->reset();
}
$stmt->execute();
}
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
/**
* An email to inform a user that a tracked article has been deleted.
*/
class NotifyArticleDeletedEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "notify-article-deleted";
/**
* @var string The name of the article that was deleted.
*/
public string $name;
/**
* Constructs an email to inform a user that a tracked article has been deleted.
*
* @param string $recipient the intended recipient of the email
* @param string $name the name of the article that was deleted
*/
public function __construct(string $recipient, string $name)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->name = $name;
$this->arg1 = $name;
}
public function getSubject(): string
{
return "$this->name article has been deleted";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
return
"The Wikipedia article about $this->name has been deleted. " .
"Death Notifier is now unable to send you a notification if $this->name dies. " .
"If the Wikipedia article is ever re-created, Death Notifier will automatically resume tracking this " .
"article, and you will receive another notification." .
"\n\n" .
"You are receiving this message because of the preferences in your Death Notifier account. " .
"To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " .
"preferences." .
"\n\n" .
$base_path;
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
/**
* An email to inform a user that a tracked article has been re-created.
*/
class NotifyArticleUndeletedEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "notify-article-undeleted";
/**
* @var string The name of the article that was re-created.
*/
public string $name;
/**
* Constructs an email to inform a user that a tracked article has been re-created.
*
* @param string $recipient the intended recipient of the email
* @param string $name the name of the article that was re-created
*/
public function __construct(string $recipient, string $name)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->name = $name;
$this->arg1 = $name;
}
public function getSubject(): string
{
return "$this->name article has been re-created";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
return
"The Wikipedia article about $this->name has been re-created. " .
"Death Notifier will once again track the article and notify you if $this->name dies." .
"\n\n" .
"You are receiving this message because of the preferences in your Death Notifier account. " .
"To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " .
"preferences." .
"\n\n" .
$base_path;
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
/**
* An email to inform a user a tracker person's status has changed.
*/
class NotifyStatusChangedEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "notify-status-changed";
/**
* @var string The name of the person whose status has changed.
*/
public string $name;
/**
* @var string The new status of the person.
*/
public string $new_status;
/**
* Constructs an email to inform a user someone has died.
*
* @param string $recipient the intended recipient of the email
* @param string $name the name of the person who died
* @param string $new_status the new status of the person
*/
public function __construct(string $recipient, string $name, string $new_status)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->name = $name;
$this->arg1 = $name;
$this->new_status = $new_status;
$this->arg2 = $new_status;
}
public function getSubject(): string
{
return "$this->name may be $this->new_status";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
return
"Someone has edited Wikipedia to state that $this->name is $this->new_status. " .
"For more information, read their Wikipedia article at " .
"https://en.wikipedia.org/wiki/" . rawurlencode($this->name) .
"\n\n" .
"You are receiving this message because of the preferences in your Death Notifier account. " .
"To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " .
"preferences." .
"\n\n" .
$base_path;
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
use com\fwdekker\deathnotifier\UserManager;
/**
* An email to be sent to a recently registered user, including instructions for email verification.
*/
class RegisterEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "register";
/**
* @var string The token to verify the email address with.
*/
public string $token;
/**
* Constructs an email to be sent to a recently registered user, including instructions for email verification.
*
* @param string $recipient the intended recipient of the email
* @param string $token the token to verify the email address with
*/
public function __construct(string $recipient, string $token)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->token = $token;
$this->arg1 = $token;
}
public function getSubject(): string
{
return "Welcome to Death Notifier";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
return
"You have successfully created an account with Death Notifier. " .
"Welcome!" .
"\n\n" .
"Until you verify your email address, you will not receive any notifications. " .
"You can verify your email address by clicking the link below. " .
"This link will expire after " . UserManager::MINUTES_VALID_VERIFICATION . " minutes." .
"\n" .
"Verify: $verify_path" .
"\n\n" .
"If you did not create this account, you can delete the account. " .
"Go to the Death Notifier website, reset your password, log in, and delete the account." .
"\n\n" .
$base_path;
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
use com\fwdekker\deathnotifier\UserManager;
/**
* An email to help a user reset their password.
*/
class ResetPasswordEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "reset-password";
/**
* @var string The token to reset the password with.
*/
public string $token;
/**
* Constructs an email to help a user reset their password.
*
* @param string $recipient the intended recipient of the email
* @param string $token the token to reset the password with
*/
public function __construct(string $recipient, string $token)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->token = $token;
$this->arg1 = $token;
}
public function getSubject(): string
{
return "Reset your password";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
$verify_path =
"$base_path?action=reset-password&email=" . rawurlencode($this->recipient) . "&token=$this->token";
return
"You requested a password reset link for your Death Notifier account. " .
"You can choose a new password by clicking the link below. " .
"This link expires after " . UserManager::MINUTES_VALID_PASSWORD_RESET . " minutes." .
"\n" .
"Reset password: $verify_path" .
"\n\n" .
"If you did not request a new password, you can safely ignore this message." .
"\n\n" .
$base_path;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace com\fwdekker\deathnotifier\mailer;
use com\fwdekker\deathnotifier\UserManager;
/**
* An email to help a user verify their email address.
*/
class VerifyEmailEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "verify-email";
/**
* @var string The token to verify the email address with.
*/
public string $token;
/**
* Constructs an email to help a user verify their email address.
*
* @param string $recipient the intended recipient of the email
* @param string $token the token to verify the email address with
*/
public function __construct(string $recipient, string $token)
{
$this->type = self::TYPE;
$this->recipient = $recipient;
$this->token = $token;
$this->arg1 = $token;
}
public function getSubject(): string
{
return "Verify your email address";
}
public function getBody(array $config): string
{
$base_path = $config["server"]["base_path"];
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
return
"You requested a new verification link for your Death Notifier account. " .
"You can verify your email address by clicking the link below. " .
"This link will expire after " . UserManager::MINUTES_VALID_VERIFICATION . " minutes. " .
"Until you verify your email address, you will not receive any notifications." .
"\n" .
"Verify: $verify_path" .
"\n\n" .
$base_path;
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace com\fwdekker\deathnotifier\trackings;
/**
* The status assigned to a person.
*/
enum PersonStatus: string
{
case Alive = "alive";
case PossiblyAlive = "possibly alive";
case Missing = "missing";
case Dead = "dead";
}

View File

@ -1,7 +1,15 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier\trackings;
use com\fwdekker\deathnotifier\ArticleType;
use com\fwdekker\deathnotifier\Database;
use com\fwdekker\deathnotifier\mailer\Mailer;
use com\fwdekker\deathnotifier\mailer\NotifyArticleDeletedEmail;
use com\fwdekker\deathnotifier\mailer\NotifyArticleUndeletedEmail;
use com\fwdekker\deathnotifier\mailer\NotifyStatusChangedEmail;
use com\fwdekker\deathnotifier\Mediawiki;
use com\fwdekker\deathnotifier\Response;
use Exception;
use Monolog\Logger;
use PDO;
@ -336,7 +344,7 @@ class TrackingManager
/**
* Updates peoples' statuses.
*
* @param array<string, array{"status": PersonStatus|null, "type": ArticleType}> $statuses the current statuses of
* @param array<string, array{"status": \com\fwdekker\deathnotifier\trackings\PersonStatus|null, "type": ArticleType}> $statuses the current statuses of
* people
* @return void
*/
@ -376,15 +384,3 @@ class TrackingManager
}
}
}
/**
* The status assigned to a person.
*/
enum PersonStatus: string
{
case Alive = "alive";
case PossiblyAlive = "possibly alive";
case Missing = "missing";
case Dead = "dead";
}

View File

@ -0,0 +1,26 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\Response;
/**
* Requires the input to be a valid email address.
*/
class IsEmailRule extends Rule
{
/**
* Checks whether the input is a valid 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
*/
public function check(array $inputs, string $key): ?Response
{
return !isset($inputs[$key]) || !filter_var($inputs[$key], FILTER_VALIDATE_EMAIL)
? Response::unsatisfied($this->override_message ?? "Enter a valid email address.", $key)
: null;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\Response;
/**
* Requires the input to not be blank.
*/
class IsNotBlankRule extends Rule
{
/**
* Checks whether 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
*/
public function check(array $inputs, string $key): ?Response
{
return !isset($inputs[$key]) || trim($inputs[$key]) === ""
? Response::unsatisfied($this->override_message ?? "Use at least one character.", $key)
: null;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\Response;
/**
* Requires the input to be set.
*/
class IsSetRule extends Rule
{
/**
* Checks whether 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
*/
public function check(array $inputs, string $key): ?Response
{
return !isset($inputs[$key])
? Response::unsatisfied($this->override_message ?? "Field '" . htmlentities($key) . "' required.", $key)
: null;
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\Response;
/**
* Requires the input to be of a specific length.
*/
class LengthRule extends Rule
{
/**
* @var int|null The minimum length (inclusive), or `null` if there is no minimum length.
*/
private readonly ?int $min_length;
/**
* @var int|null The maximum length (inclusive), or `null` if there is no maximum length.
*/
private readonly ?int $max_length;
/**
* Instantiates a new rule.
*
* @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
* @param string|null $override_message the message to return if the rule does not apply to some input. If `null`,
* the rule implementation can choose an appropriate message
*/
public function __construct(?int $min_length = null, ?int $max_length = null, ?string $override_message = null)
{
parent::__construct($override_message);
$this->min_length = $min_length;
$this->max_length = $max_length;
}
/**
* Checks whether the input is of the specified 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
*/
public function check(array $inputs, string $key): ?Response
{
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(
$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(
$this->override_message ?? "Use at most $this->max_length character(s).",
$key
);
else
return null;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\Response;
/**
* A rule/constraint/assertion that should hold over an input.
*/
abstract class Rule
{
/**
* @var string|null The message to return if the rule does not apply to some input. If `null`, the rule
* implementation can choose an appropriate message.
*/
public ?string $override_message;
/**
* Instantiates a new rule.
*
* @param string|null $override_message the message to return if the rule does not apply to some input. If `null`,
* the rule implementation can choose an appropriate message
*/
public function __construct(?string $override_message = null)
{
$this->override_message = $override_message;
}
/**
* Checks whether the rule holds for `$inputs[$key]`.
*
* Implementations should never assume that the `$inputs[$key]` is set.
*
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
* @param string $key the key in `inputs` of the input to check
* @return Response|null `null` if the rule holds, or an unsatisfied `Response` otherwise
*/
public abstract function check(array $inputs, string $key): ?Response;
}

View File

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

@ -1,7 +1,9 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\mailer\Mailer;
use com\fwdekker\deathnotifier\trackings\TrackingManager;
use Exception;
use Monolog\Logger;
use Monolog\Test\TestCase;

View File

@ -1,7 +1,9 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\mailer\Mailer;
use com\fwdekker\deathnotifier\mailer\RegisterEmail;
use PHPUnit\Framework\MockObject\MockObject;

View File

@ -1,12 +1,12 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier\validator;
/**
* Unit tests for `IsEmailRule`.
*/
final class IsEmailRuleTest extends RuleTest
class IsEmailRuleTest extends RuleTest
{
function get_rule(?string $override = null): Rule
{
@ -37,7 +37,7 @@ final class IsEmailRuleTest extends RuleTest
{
$rule = new IsEmailRule();
$is_valid = $rule->check(["email" => "exampleexample.com"], "email");
$is_valid = $rule->check(["email" => "example.com"], "email");
$this->assertNotNull($is_valid);
$this->assertEquals("Enter a valid email address.", $is_valid->payload["message"]);

View File

@ -1,12 +1,12 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier\validator;
/**
* Unit tests for `IsNotBlankRule`.
*/
final class IsNotBlankRuleTest extends RuleTest
class IsNotBlankRuleTest extends RuleTest
{
function get_rule(?string $override = null): Rule
{

View File

@ -1,12 +1,12 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier\validator;
/**
* Unit tests for `IsSetRule`.
*/
final class IsSetRuleTest extends RuleTest
class IsSetRuleTest extends RuleTest
{
function get_rule(?string $override = null): Rule
{

View File

@ -1,12 +1,12 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier\validator;
/**
* Unit tests for `LengthRule`.
*/
final class LengthRuleTest extends RuleTest
class LengthRuleTest extends RuleTest
{
function get_rule(?string $override = null): Rule
{

View File

@ -1,6 +1,6 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier\validator;
use Monolog\Test\TestCase;

View File

@ -1,6 +1,6 @@
<?php
namespace php;
namespace com\fwdekker\deathnotifier\validator;
use Monolog\Test\TestCase;
@ -8,7 +8,7 @@ use Monolog\Test\TestCase;
/**
* Unit tests for `Validator`.
*/
final class ValidatorTest extends TestCase
class ValidatorTest extends TestCase
{
function test_validate_inputs_returns_null_if_there_are_no_rule_sets(): void
{