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:
parent
379498cfe0
commit
43feaa0fed
15
Gruntfile.js
15
Gruntfile.js
|
@ -17,7 +17,13 @@ module.exports = grunt => {
|
||||||
files: [{expand: true, cwd: "src/main/", src: "**/*.html", dest: "dist/", flatten: true}]
|
files: [{expand: true, cwd: "src/main/", src: "**/*.html", dest: "dist/", flatten: true}]
|
||||||
},
|
},
|
||||||
php: {
|
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: {
|
focus: {
|
||||||
|
@ -63,7 +69,7 @@ module.exports = grunt => {
|
||||||
exec: "composer.phar install --no-dev"
|
exec: "composer.phar install --no-dev"
|
||||||
},
|
},
|
||||||
phpunit: {
|
phpunit: {
|
||||||
exec: "vendor/bin/phpunit --testdox src/test"
|
exec: "cd dist/ && chmod +x .vendor/bin/phpunit && .vendor/bin/phpunit --testdox ."
|
||||||
},
|
},
|
||||||
stan: {
|
stan: {
|
||||||
exec: "vendor/bin/phpstan analyse -l 8 src/main src/test"
|
exec: "vendor/bin/phpstan analyse -l 8 src/main src/test"
|
||||||
|
@ -79,8 +85,8 @@ module.exports = grunt => {
|
||||||
tasks: ["copy:html", "replace:dev"],
|
tasks: ["copy:html", "replace:dev"],
|
||||||
},
|
},
|
||||||
php: {
|
php: {
|
||||||
files: ["src/main/**/*.php"],
|
files: ["src/**/*.php"],
|
||||||
tasks: ["copy:php", "replace:dev"],
|
tasks: ["copy:php", "copy:php_test", "replace:dev"],
|
||||||
},
|
},
|
||||||
ts: {
|
ts: {
|
||||||
files: ["src/main/**/*.ts"],
|
files: ["src/main/**/*.ts"],
|
||||||
|
@ -135,6 +141,7 @@ module.exports = grunt => {
|
||||||
"copy:css",
|
"copy:css",
|
||||||
"copy:html",
|
"copy:html",
|
||||||
"copy:php",
|
"copy:php",
|
||||||
|
"copy:php_test",
|
||||||
// Compile JS
|
// Compile JS
|
||||||
"webpack:dev",
|
"webpack:dev",
|
||||||
"replace:dev",
|
"replace:dev",
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"php\\": "php/"
|
"com\\fwdekker\\deathnotifier\\": "com/fwdekker/deathnotifier/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimum-stability": "stable"
|
"minimum-stability": "stable"
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use php\Database;
|
use com\fwdekker\deathnotifier\Database;
|
||||||
use php\IsEmailRule;
|
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||||
use php\IsNotBlankRule;
|
use com\fwdekker\deathnotifier\Mediawiki;
|
||||||
use php\IsSetRule;
|
use com\fwdekker\deathnotifier\Response;
|
||||||
use php\LengthRule;
|
use com\fwdekker\deathnotifier\trackings\TrackingManager;
|
||||||
use php\Mailer;
|
use com\fwdekker\deathnotifier\UserManager;
|
||||||
use php\Mediawiki;
|
use com\fwdekker\deathnotifier\Util;
|
||||||
use php\Response;
|
use com\fwdekker\deathnotifier\validator\IsEmailRule;
|
||||||
use php\TrackingManager;
|
use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
|
||||||
use php\UserManager;
|
use com\fwdekker\deathnotifier\validator\IsSetRule;
|
||||||
use php\Util;
|
use com\fwdekker\deathnotifier\validator\LengthRule;
|
||||||
use php\Validator;
|
use com\fwdekker\deathnotifier\validator\Validator;
|
||||||
|
|
||||||
/** @noinspection PhpIncludeInspection Exists after `npm run deploy` */
|
/** @noinspection PhpIncludeInspection Exists after `npm run deploy` */
|
||||||
require_once __DIR__ . "/.vendor/autoload.php";
|
require_once __DIR__ . "/.vendor/autoload.php";
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace php;
|
namespace com\fwdekker\deathnotifier;
|
||||||
|
|
||||||
|
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||||
|
use com\fwdekker\deathnotifier\trackings\TrackingManager;
|
||||||
use Composer\Semver\Comparator;
|
use Composer\Semver\Comparator;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use PDO;
|
use PDO;
|
|
@ -1,7 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace php;
|
namespace com\fwdekker\deathnotifier;
|
||||||
|
|
||||||
|
use com\fwdekker\deathnotifier\trackings\PersonStatus;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace php;
|
namespace com\fwdekker\deathnotifier;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,7 +1,13 @@
|
||||||
<?php
|
<?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 DateTime;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use PDO;
|
use PDO;
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace php;
|
namespace com\fwdekker\deathnotifier;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Monolog\ErrorHandler;
|
use Monolog\ErrorHandler;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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."),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
|
@ -1,7 +1,15 @@
|
||||||
<?php
|
<?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 Exception;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use PDO;
|
use PDO;
|
||||||
|
@ -336,7 +344,7 @@ class TrackingManager
|
||||||
/**
|
/**
|
||||||
* Updates peoples' statuses.
|
* 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
|
* people
|
||||||
* @return void
|
* @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";
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace php;
|
namespace com\fwdekker\deathnotifier;
|
||||||
|
|
||||||
|
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||||
|
use com\fwdekker\deathnotifier\trackings\TrackingManager;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use Monolog\Test\TestCase;
|
use Monolog\Test\TestCase;
|
|
@ -1,7 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace php;
|
namespace com\fwdekker\deathnotifier;
|
||||||
|
|
||||||
|
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||||
|
use com\fwdekker\deathnotifier\mailer\RegisterEmail;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace php;
|
namespace com\fwdekker\deathnotifier\validator;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for `IsEmailRule`.
|
* Unit tests for `IsEmailRule`.
|
||||||
*/
|
*/
|
||||||
final class IsEmailRuleTest extends RuleTest
|
class IsEmailRuleTest extends RuleTest
|
||||||
{
|
{
|
||||||
function get_rule(?string $override = null): Rule
|
function get_rule(?string $override = null): Rule
|
||||||
{
|
{
|
||||||
|
@ -37,7 +37,7 @@ final class IsEmailRuleTest extends RuleTest
|
||||||
{
|
{
|
||||||
$rule = new IsEmailRule();
|
$rule = new IsEmailRule();
|
||||||
|
|
||||||
$is_valid = $rule->check(["email" => "exampleexample.com"], "email");
|
$is_valid = $rule->check(["email" => "example.com"], "email");
|
||||||
|
|
||||||
$this->assertNotNull($is_valid);
|
$this->assertNotNull($is_valid);
|
||||||
$this->assertEquals("Enter a valid email address.", $is_valid->payload["message"]);
|
$this->assertEquals("Enter a valid email address.", $is_valid->payload["message"]);
|
|
@ -1,12 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace php;
|
namespace com\fwdekker\deathnotifier\validator;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for `IsNotBlankRule`.
|
* Unit tests for `IsNotBlankRule`.
|
||||||
*/
|
*/
|
||||||
final class IsNotBlankRuleTest extends RuleTest
|
class IsNotBlankRuleTest extends RuleTest
|
||||||
{
|
{
|
||||||
function get_rule(?string $override = null): Rule
|
function get_rule(?string $override = null): Rule
|
||||||
{
|
{
|
|
@ -1,12 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace php;
|
namespace com\fwdekker\deathnotifier\validator;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for `IsSetRule`.
|
* Unit tests for `IsSetRule`.
|
||||||
*/
|
*/
|
||||||
final class IsSetRuleTest extends RuleTest
|
class IsSetRuleTest extends RuleTest
|
||||||
{
|
{
|
||||||
function get_rule(?string $override = null): Rule
|
function get_rule(?string $override = null): Rule
|
||||||
{
|
{
|
|
@ -1,12 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace php;
|
namespace com\fwdekker\deathnotifier\validator;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for `LengthRule`.
|
* Unit tests for `LengthRule`.
|
||||||
*/
|
*/
|
||||||
final class LengthRuleTest extends RuleTest
|
class LengthRuleTest extends RuleTest
|
||||||
{
|
{
|
||||||
function get_rule(?string $override = null): Rule
|
function get_rule(?string $override = null): Rule
|
||||||
{
|
{
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace php;
|
namespace com\fwdekker\deathnotifier\validator;
|
||||||
|
|
||||||
use Monolog\Test\TestCase;
|
use Monolog\Test\TestCase;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace php;
|
namespace com\fwdekker\deathnotifier\validator;
|
||||||
|
|
||||||
use Monolog\Test\TestCase;
|
use Monolog\Test\TestCase;
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ use Monolog\Test\TestCase;
|
||||||
/**
|
/**
|
||||||
* Unit tests for `Validator`.
|
* 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
|
function test_validate_inputs_returns_null_if_there_are_no_rule_sets(): void
|
||||||
{
|
{
|
Loading…
Reference in New Issue