Clarify several error messages and signatures

This commit is contained in:
Florine W. Dekker 2022-12-07 19:36:14 +01:00
parent fbce900475
commit c49cd7184f
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
14 changed files with 116 additions and 75 deletions

View File

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

BIN
composer.lock generated

Binary file not shown.

BIN
package-lock.json generated

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "death-notifier",
"version": "0.17.3", "_comment_version": "Also update version in `composer.json`!",
"version": "0.17.4", "_comment_version": "Also update version in `composer.json`!",
"description": "Get notified when a famous person dies.",
"author": "Florine W. Dekker",
"browser": "dist/bundle.js",

View File

@ -5,7 +5,6 @@ use com\fwdekker\deathnotifier\ActionMethod;
use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\Database;
use com\fwdekker\deathnotifier\EmulateCronAction;
use com\fwdekker\deathnotifier\IllegalArgumentError;
use com\fwdekker\deathnotifier\LoggerUtil;
use com\fwdekker\deathnotifier\mailer\EmailQueue;
use com\fwdekker\deathnotifier\mailer\ProcessEmailQueueAction;

View File

@ -3,7 +3,6 @@
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use InvalidArgumentException;
use Monolog\Logger;

View File

@ -68,12 +68,12 @@ class Util
*
* For example, if {@see $timestamp} was 5 minutes ago, and {@see $interval} is 7, then this function returns 2.
*
* @param string $timestamp the timestamp at which some event occurred
* @param int $timestamp the timestamp at which some event occurred
* @param int $interval the number of minutes to measure against
* @return int the number of minutes until {@see $timestamp} was {@see $interval} minutes ago
*/
static function minutes_until_interval_elapsed(string $timestamp, int $interval): int
static function minutes_until_interval_elapsed(int $timestamp, int $interval): int
{
return $interval - ((time() - intval($timestamp)) / 60);
return $interval - ((time() - $timestamp) / 60);
}
}

View File

@ -5,7 +5,6 @@ namespace com\fwdekker\deathnotifier\tracking;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
use com\fwdekker\deathnotifier\validator\IsStringRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;

View File

@ -3,7 +3,6 @@
namespace com\fwdekker\deathnotifier\tracking;
use com\fwdekker\deathnotifier\Database;
use com\fwdekker\deathnotifier\wikipedia\ArticleType;
use com\fwdekker\deathnotifier\wikipedia\PersonStatus;
use PDO;
@ -256,17 +255,16 @@ class TrackingList
$new_deletions = [];
$this->transaction(function () use ($deletions, &$new_deletions) {
$delete = $this->database->conn->prepare("UPDATE people
SET is_deleted=1
WHERE name=:name AND is_deleted<>1
RETURNING name;");
$delete->bindParam(":name", $deleted_name);
$stmt = $this->database->conn->prepare("UPDATE people
SET is_deleted=1
WHERE name=:name AND is_deleted<>1
RETURNING name;");
$stmt->bindParam(":name", $deleted_name);
foreach ($deletions as $deleted_name) {
$delete->execute();
$newly_deleted = sizeof($delete->fetchAll(PDO::FETCH_ASSOC)) > 0;
$stmt->execute();
if ($newly_deleted)
if (sizeof($stmt->fetchAll(PDO::FETCH_ASSOC)) > 0)
$new_deletions[] = $deleted_name;
}
});
@ -275,51 +273,63 @@ class TrackingList
}
/**
* Updates peoples' statuses.
* Marks people as undeleted in the database.
*
* @param array<string, array{"type": ArticleType, "status": PersonStatus|null}> $statuses the current statuses of
* people
* @return array{string[], array<string, string>} the list of articles that were actually undeleted, and a mapping
* of articles that were actually changes to the new status
* @param string[] $undeletions list of names of people to mark as undeleted in the database
* @return string[] subset of {@see $undeletions} containing the names of people that were newly marked as undeleted
*/
public function update_statuses(array $statuses): array
public function undelete_persons(array $undeletions): array
{
$undeletions = [];
$status_changes = [];
$new_undeletions = [];
$this->transaction(function () use ($statuses, &$undeletions, &$status_changes) {
$conn = $this->database->conn;
$this->transaction(function () use ($undeletions, &$new_undeletions) {
$stmt = $this->database->conn->prepare("UPDATE people
SET is_deleted=0
WHERE name=:name AND is_deleted<>0
RETURNING name;");
$stmt->bindParam(":name", $undeleted_name);
// Query to mark person as no longer deleted, returning `name` to determine whether something changed
// TODO: Split this into two methods, one for `undelete`, one for `update_statuses`?
$undelete = $conn->prepare("UPDATE people
SET is_deleted=0
WHERE name=:name AND is_deleted<>0
RETURNING name;");
$undelete->bindParam(":name", $person_name);
foreach ($undeletions as $undeleted_name) {
$stmt->execute();
// Query to update status, returning `name` to determine whether something changed
$set_status = $conn->prepare("UPDATE people
SET status=:status
WHERE name=:name AND status<>:status
RETURNING name;");
$set_status->bindParam(":status", $person_status_string);
$set_status->bindParam(":name", $person_name);
foreach ($statuses as $person_name => $person_info) {
if ($person_info["status"] === null) continue;
$person_status_string = $person_info["status"]->value;
$undelete->execute();
$undeleted = sizeof($undelete->fetchAll(PDO::FETCH_ASSOC)) > 0;
if ($undeleted) $undeletions[] = $person_name;
$set_status->execute();
$status_changed = sizeof($set_status->fetchAll(PDO::FETCH_ASSOC)) > 0;
if ($status_changed) $status_changes[$person_name] = $person_status_string;
if (sizeof($stmt->fetchAll(PDO::FETCH_ASSOC)) > 0)
$new_undeletions[] = $undeleted_name;
}
});
return [$undeletions, $status_changes];
return $new_undeletions;
}
/**
* Updates peoples' statuses.
*
* @param array<string, PersonStatus|null> $statuses the current statuses of
* people
* @return array<string, string> a mapping of articles that were actually changes to the new status
*/
public function update_statuses(array $statuses): array
{
$status_changes = [];
$this->transaction(function () use ($statuses, &$status_changes) {
$stmt = $this->database->conn->prepare("UPDATE people
SET status=:status
WHERE name=:name AND status<>:status
RETURNING name;");
$stmt->bindParam(":status", $person_status_string);
$stmt->bindParam(":name", $person_name);
foreach ($statuses as $person_name => $person_status) {
if ($person_status === null) continue;
$person_status_string = $person_status->value;
$stmt->execute();
if (sizeof($stmt->fetchAll(PDO::FETCH_ASSOC)) > 0)
$status_changes[$person_name] = $person_status_string;
}
});
return $status_changes;
}
}

View File

@ -74,9 +74,9 @@ class UpdateTrackingsAction extends Action
// Process changes
$new_deletions = [];
$undeletions = [];
$status_changes = [];
$this->tracking_list->transaction(function () use (&$new_deletions, &$undeletions, &$status_changes) {
$new_undeletions = [];
$new_status_changes = [];
$this->tracking_list->transaction(function () use (&$new_deletions, &$new_undeletions, &$new_status_changes) {
$names = $this->tracking_list->list_all_unique_person_names();
if (empty($names))
return;
@ -90,7 +90,13 @@ class UpdateTrackingsAction extends Action
$this->tracking_list->rename_persons($people_statuses->redirects);
$new_deletions = $this->tracking_list->delete_persons($people_statuses->missing);
[$undeletions, $status_changes] = $this->tracking_list->update_statuses($people_statuses->results);
$new_undeletions = $this->tracking_list->undelete_persons(array_keys($people_statuses->results));
$new_status_changes = $this->tracking_list->update_statuses(
array_combine(
array_keys($people_statuses->results),
array_map(fn($it) => $it["status"], $people_statuses->results)
)
);
});
// Send mails
@ -100,11 +106,11 @@ class UpdateTrackingsAction extends Action
foreach ($this->tracking_list->list_trackers($new_deletion) as $user_email)
$this->mailer->queue_email(new NotifyArticleDeletedEmail($user_email, $new_deletion));
foreach ($undeletions as $undeletion)
foreach ($new_undeletions as $undeletion)
foreach ($this->tracking_list->list_trackers($undeletion) as $user_email)
$this->mailer->queue_email(new NotifyArticleUndeletedEmail($user_email, $undeletion));
foreach ($status_changes as $person_name => $person_status)
foreach ($new_status_changes as $person_name => $person_status)
foreach ($this->tracking_list->list_trackers($person_name) as $user_email)
$this->mailer->queue_email(new NotifyStatusChangedEmail($user_email, $person_name, $person_status));

View File

@ -6,7 +6,6 @@ use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\validator\IsBooleanRule;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\validator\IsSetRule;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;

View File

@ -150,13 +150,25 @@ class UserList
* Returns all data of the user with the given UUID.
*
* @param string $uuid the UUID of the user to return the data of
* @return array<string, mixed>|null all data of the user with the given UUID, or `null` if the user could not be
* found
* @return array{"uuid": string, "email": string, "email_verification_token": string|null,
* "email_verification_token_timestamp": int, "email_notifications_enabled": int, "password": string,
* "password_last_change": int, "password_reset_token": string|null,
* "password_reset_token_timestamp": int}|null all data of the user with the given UUID, or `null` if the user
* could not be found
*/
// TODO: Specify the return signature *exactly*
public function get_user_by_uuid(string $uuid): ?array
{
$stmt = $this->database->conn->prepare("SELECT * FROM users WHERE uuid=:uuid;");
$stmt = $this->database->conn->prepare("SELECT uuid,
email,
email_verification_token,
email_verification_token_timestamp,
email_notifications_enabled,
password,
password_last_change,
password_reset_token,
password_reset_token_timestamp
FROM users
WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
@ -167,12 +179,25 @@ class UserList
* Returns all data of the user with the given email address.
*
* @param string $email the email address of the user to return the data of
* @return array<string, mixed>|null all data of the user with the given email address, or `null` if the user could
* not be found
* @return array{"uuid": string, "email": string, "email_verification_token": string|null,
* "email_verification_token_timestamp": int, "email_notifications_enabled": int, "password": string,
* "password_last_change": int, "password_reset_token": string|null,
* "password_reset_token_timestamp": int}|null all data of the user with the given email address, or `null` if
* the user could not be found
*/
public function get_user_by_email(string $email): ?array
{
$stmt = $this->database->conn->prepare("SELECT * FROM users WHERE email=:email;");
$stmt = $this->database->conn->prepare("SELECT uuid,
email,
email_verification_token,
email_verification_token_timestamp,
email_notifications_enabled,
password,
password_last_change,
password_reset_token,
password_reset_token_timestamp
FROM users
WHERE email=:email;");
$stmt->bindValue(":email", $email);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);

View File

@ -72,10 +72,11 @@ class ValidatePasswordResetTokenAction extends Action
if ($user_data === null)
throw new InvalidInputException("No user with that email address has been registered.");
if ($reset_token !== $user_data["password_reset_token"])
// TODO: Just tell the user why the link is invalid: Because no request exists, or because the token is wrong
// TODO: Also, tell the user what they can do to resolve this
throw new InvalidInputException(
"This password reset link is invalid. Maybe you already reset your password?"
"This password reset link is invalid. " .
"This may happen if you recently changed your email address or you requested another " .
"password reset link, in which case a new password reset link should arrive in your inbox soon. " .
"If this does not happen, log in and request a new password reset email."
);
$minutes_left = Util::minutes_until_interval_elapsed(

View File

@ -56,11 +56,14 @@ class VerifyEmailAction extends Action
$user_data = $this->user_list->get_user_by_email($inputs["email"]);
if ($user_data === null)
throw new InvalidInputException("No user with that email address has been registered.");
if ($user_data["email_verification_token"] !== $inputs["verify_token"])
// TODO: Just tell the user whether the account has been verified or whether the token is plain wrong
// TODO: Also, tell the user what they can do to resolve this
if ($user_data["email_verification_token"] === null)
throw new InvalidInputException("Your email address is already verified. You can close this page.");
if ($inputs["verify_token"] !== $user_data["email_verification_token"])
throw new InvalidInputException(
"Failed to verify email address. Maybe you already verified your email address?"
"This verification link is invalid. " .
"This may happen if you recently changed your email address or you requested another " .
"verification link, in which case a new verification link should arrive in your inbox soon. " .
"If this does not happen, log in and request a new verification email."
);
$minutes_left = Util::minutes_until_interval_elapsed(