Restructure validation logic

This commit is contained in:
Florine W. Dekker 2022-12-08 18:37:26 +01:00
parent 732298bf09
commit 12e7437335
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
59 changed files with 808 additions and 494 deletions

View File

@ -1,7 +1,7 @@
{
"name": "fwdekker/death-notifier",
"description": "Get notified when a famous person dies.",
"version": "0.19.0", "_comment_version": "Also update version in `package.json`!",
"version": "0.19.1", "_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.19.0", "_comment_version": "Also update version in `composer.json`!",
"version": "0.19.1", "_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

@ -35,5 +35,7 @@ allow_config_insecure_permissions = false
[server]
# The path to the directory containing the site's main page.
base_path = https://example.com/death-notifier/
# The path at which users can report bugs, or an empty string if there is no such path.
issue_path =
# The message to display at the top of all pages. A blank string hides the message.
global_message =

View File

@ -237,7 +237,9 @@
<th></th>
</tr>
</thead>
<tbody></tbody>
<tbody>
<!-- TODO: Show loading icon while fetching entries -->
</tbody>
</table>
</div>
<form id="add-tracking-form" novalidate>

View File

@ -2,7 +2,8 @@
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
/**
@ -17,7 +18,8 @@ abstract class Action
*
* @param array<int|string, mixed> $inputs the inputs to perform the action with
* @return mixed the requested data; may be `null`
* @throws InvalidInputException if any of the inputs is invalid
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if any of the inputs is invalid
* @throws UnexpectedException if the action could not be performed even though the inputs are valid
*/
abstract function handle(array $inputs): mixed;

View File

@ -2,7 +2,8 @@
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use Monolog\Logger;
@ -75,8 +76,8 @@ class ActionDispatcher
* @param array<int|string, mixed> $inputs the inputs to the action, where the `action` key specifies which action
* to execute
* @return Response an unsatisfied `Response` if no registered {@see Action} could be found to handle the request,
* an unsatisfied `Response` if an `Action` could be found but threw an {@see UnexpectedException} or an
* {@see InvalidInputException}, or a satisfied `Response` containing the return value of `Action` that was invoked
* or if an `Action` could be found but threw an {@see InvalidTypeException}, {@see InvalidValueException}, or
* {@see UnexpectedException}, or a satisfied `Response` containing the return value of `Action` that was invoked
* with {@see $inputs}
*/
public function handle(ActionMethod $method, array $inputs): Response
@ -98,8 +99,30 @@ class ActionDispatcher
} catch (UnexpectedException $exception) {
$this->logger->error($exception);
return Response::unsatisfied(message: $exception->getMessage());
} catch (InvalidInputException $exception) {
} catch (InvalidTypeException $exception) {
return Response::unsatisfied(message: $this->format_invalid_type_message($exception));
} catch (InvalidValueException $exception) {
return Response::unsatisfied(message: $exception->getMessage(), target: $exception->target);
}
}
/**
* Returns a user-friendly message to display if an {@see InvalidTypeException} is thrown.
*
* @param InvalidTypeException $exception the exception to format
* @return string a user-friendly message to display if an {@see InvalidTypeException} is thrown
*/
private function format_invalid_type_message(InvalidTypeException $exception): string
{
$message = "Malformed request: " . $exception->getMessage() . " ";
$issue_path = Config::get("server.issue_path");
if ($issue_path === null)
$message .= "This is a bug. Reload the page and try again.";
else
$message .= `This is a bug. You can report this bug at <a href="$issue_path">the issue tracker</a>.`;
return $message;
}
}

View File

@ -27,7 +27,7 @@ class Config
*
* @param string|null $index the index of the property to return, using `.` as a nesting delimiter; or `null` to
* return the entire configuration
* @return mixed the property at {@see $index}
* @return mixed|null the property at {@see $index}, or `null` if there is no such property
*/
public static function get(?string $index = null): mixed
{
@ -38,8 +38,12 @@ class Config
return self::$config;
$output = self::$config;
foreach (explode(".", $index) as $key)
foreach (explode(".", $index) as $key) {
if (!isset($output[$key]))
return null;
$output = $output[$key];
}
return $output;
}
@ -51,16 +55,7 @@ class Config
*/
public static function has(string $index): bool
{
$output = self::get();
foreach (explode(".", $index) as $key) {
if (!isset($output[$key]))
return false;
$output = $output[$key];
}
return true;
return self::get($index) !== null;
}

View File

@ -2,7 +2,7 @@
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
/**
@ -39,7 +39,7 @@ class EmulateCronAction extends Action
*
* @param array<int|string, mixed> $inputs the inputs to perform the action with
* @return never
* @throws InvalidInputException if any of the inputs is invalid
* @throws InvalidValueException if any of the inputs is invalid
* @throws UnexpectedException if the action could not be performed even though the inputs are valid
* @noinspection PhpDocRedundantThrowsInspection may be thrown by {@see $actions}
*/
@ -47,6 +47,7 @@ class EmulateCronAction extends Action
{
// @phpstan-ignore-next-line
while (true) {
// TODO: Log last cron task in database, to make it really clear when it last happened
foreach ($this->actions as $action)
$action->handle($inputs);
print("Done.\n");

View File

@ -2,7 +2,7 @@
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use Exception;
use JsonException;
@ -16,7 +16,7 @@ class Util
* Parses POST values from JSON-based inputs.
*
* @return array<int|string, mixed> the parsed POSTed values, or an empty array if no values were POSTed
* @throws InvalidInputException if there are no POST input values, or if the POST input is not valid JSON
* @throws InvalidValueException if there are no POST input values, or if the POST input is not valid JSON
*/
static function parse_post(): array
{
@ -27,11 +27,11 @@ class Util
try {
$post = json_decode($post_input, associative: true, flags: JSON_THROW_ON_ERROR);
if ($post === null)
throw new InvalidInputException("Malformed request: POST data is `null`.");
throw new InvalidValueException("Malformed request: POST data is `null`.");
return $post;
} catch (JsonException) {
throw new InvalidInputException("Malformed request: POST data could not be parsed as JSON.");
throw new InvalidValueException("Malformed request: POST data could not be parsed as JSON.");
}
}

View File

@ -5,9 +5,10 @@ namespace com\fwdekker\deathnotifier\mailer;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\validator\EqualsCliPasswordRule;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validation\EqualsCliPasswordRule;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\RuleSet;
use PHPMailer\PHPMailer\Exception as PHPMailerException;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
@ -40,7 +41,8 @@ class ProcessEmailQueueAction extends Action
*
* @param array<int|string, mixed> $inputs `"password": string`: the CLI password
* @return null
* @throws InvalidInputException if the CLI password is wrong
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the CLI password is wrong
* @throws UnexpectedException if the mailer fails to send an email
*/
public function handle(array $inputs): mixed

View File

@ -5,12 +5,13 @@ namespace com\fwdekker\deathnotifier\tracking;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\IllegalStateError;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\validator\HasStringLengthRule;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;
use com\fwdekker\deathnotifier\validation\HasStringLengthRule;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsNotBlankRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
use com\fwdekker\deathnotifier\wikipedia\ArticleType;
use com\fwdekker\deathnotifier\wikipedia\PersonStatus;
use com\fwdekker\deathnotifier\wikipedia\Wikipedia;
@ -54,14 +55,15 @@ class AddTrackingAction extends Action
* of the person to track
* @return array{"input_name": string, "normalized_name": string} the person's name as given by the user, and the
* normalized version of that name
* @throws InvalidInputException if the user is not logged in, if no valid CSRF token is present, if the article
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is not logged in, if no valid CSRF token is present, if the article
* title is a blank string, if the article title is too short or too long, if the specified article does not exist,
* if the specified article is not about a person, or if the user is already tracking this article
* @throws UnexpectedException if Wikipedia could not be reached
*/
public function handle(array $inputs): array
{
(new SessionRuleSet(validate_logged_in: true))->check($_SESSION);
(new LoginValidator(validate_logged_in: true))->check($_SESSION);
(new RuleSet([
"token" => [new IsValidCsrfTokenRule()],
"person_name" => [
@ -73,7 +75,7 @@ class AddTrackingAction extends Action
[$normalized_name, $status] = $this->get_and_validate_page_info(strval($inputs["person_name"]));
$this->tracking_list->transaction(function () use ($normalized_name, $status) {
if ($this->tracking_list->has_tracking($_SESSION["uuid"], $normalized_name))
throw new InvalidInputException("You are already tracking <b>$normalized_name</b>.");
throw new InvalidValueException("You are already tracking <b>$normalized_name</b>.");
$this->tracking_list->add_tracking($_SESSION["uuid"], $normalized_name, $status);
});
@ -86,7 +88,7 @@ class AddTrackingAction extends Action
*
* @param string $person_name the title of the article about a person to return the information of
* @return array{string, PersonStatus} the normalized name and `PersonStatus` of the specified article
* @throws InvalidInputException if the article about {@see $person_name} does not exist or is not about a person
* @throws InvalidValueException if the article about {@see $person_name} does not exist or is not about a person
* @throws UnexpectedException if Wikipedia could not be reached
*/
private function get_and_validate_page_info(string $person_name): array
@ -105,7 +107,7 @@ class AddTrackingAction extends Action
}
if (in_array($normalized_name, $info->missing)) {
throw new InvalidInputException(
throw new InvalidValueException(
$this->override_message ??
"Wikipedia does not have an article about " .
"<b><a href='https://en.wikipedia.org/wiki/Special:Search?search=" .
@ -114,7 +116,7 @@ class AddTrackingAction extends Action
"person_name"
);
} else if ($type === ArticleType::Disambiguation) {
throw new InvalidInputException(
throw new InvalidValueException(
$this->override_message ??
"<b><a href='https://en.wikipedia.org/wiki/" . rawurlencode($normalized_name) . "'>" .
htmlentities($normalized_name) . "</a></b> refers to multiple articles. " .
@ -123,7 +125,7 @@ class AddTrackingAction extends Action
"person_name"
);
} else if ($type === ArticleType::Other) {
throw new InvalidInputException(
throw new InvalidValueException(
$this->override_message ??
"The Wikipedia article about " .
"<b><a href='https://en.wikipedia.org/wiki/" . rawurlencode($normalized_name) . "'>" .

View File

@ -3,10 +3,11 @@
namespace com\fwdekker\deathnotifier\tracking;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
/**
@ -38,11 +39,12 @@ class ListTrackingsAction extends Action
*
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token
* @return array<array{"name": string, "status": string, "is_deleted": bool}> all trackings of the current user
* @throws InvalidInputException if the user is not logged in or if no valid CSRF token is present
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is not logged in or if no valid CSRF token is present
*/
public function handle(array $inputs): array
{
(new SessionRuleSet(validate_logged_in: true))->check($_SESSION);
(new LoginValidator(validate_logged_in: true))->check($_SESSION);
(new RuleSet(["token" => [new IsValidCsrfTokenRule()]]))->check($inputs);
return $this->tracking_list->list_trackings($_SESSION["uuid"]);

View File

@ -3,11 +3,12 @@
namespace com\fwdekker\deathnotifier\tracking;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsStringRule;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsStringRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
/**
@ -40,12 +41,12 @@ class RemoveTrackingAction extends Action
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token, `"person_name": string`: the name
* of the person to stop tracking
* @return null
* @throws InvalidInputException if the user is not logged in, if no valid CSRF token is present, or if the article
* title is not a string
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is not logged in or if no valid CSRF token is present
*/
public function handle(array $inputs): mixed
{
(new SessionRuleSet(validate_logged_in: true))->check($_SESSION);
(new LoginValidator(validate_logged_in: true))->check($_SESSION);
(new RuleSet([
"token" => [new IsValidCsrfTokenRule()],
"person_name" => [new IsStringRule()],

View File

@ -8,9 +8,10 @@ use com\fwdekker\deathnotifier\LoggerUtil;
use com\fwdekker\deathnotifier\mailer\Email;
use com\fwdekker\deathnotifier\mailer\EmailQueue;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\validator\EqualsCliPasswordRule;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validation\EqualsCliPasswordRule;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\wikipedia\Wikipedia;
use com\fwdekker\deathnotifier\wikipedia\WikipediaException;
use Monolog\Logger;
@ -64,7 +65,8 @@ class UpdateTrackingsAction extends Action
*
* @param array<int|string, mixed> $inputs `"password": string`: the CLI password
* @return null
* @throws InvalidInputException if the CLI password is wrong
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the CLI password is wrong
* @throws UnexpectedException if Wikipedia could not be reached
* @noinspection PhpDocRedundantThrowsInspection can be thrown through {@see TrackingList::transaction()}
*/

View File

@ -8,11 +8,12 @@ use com\fwdekker\deathnotifier\mailer\Email;
use com\fwdekker\deathnotifier\mailer\EmailQueue;
use com\fwdekker\deathnotifier\tracking\TrackingList;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsEmailRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
/**
@ -54,14 +55,15 @@ class ChangeEmailAction extends Action
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token, `"email": string`: the new email
* address
* @return null
* @throws InvalidInputException if the user is not logged in, if no valid CSRF token is present, if the email
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is not logged in, if no valid CSRF token is present, if the email
* address is invalid, if the email address is not different, or if the email address is already used
* @throws UnexpectedException if the current user has been deleted
* @noinspection PhpDocRedundantThrowsInspection can be thrown through {@see TrackingList::transaction()}
*/
public function handle(array $inputs): mixed
{
(new SessionRuleSet(validate_logged_in: true))->check($_SESSION);
(new LoginValidator(validate_logged_in: true))->check($_SESSION);
(new RuleSet([
"token" => [new IsValidCsrfTokenRule()],
"email" => [new IsEmailRule()],
@ -70,11 +72,11 @@ class ChangeEmailAction extends Action
$this->user_list->transaction(function () use ($inputs) {
$user_data = $this->user_list->get_user_by_uuid($_SESSION["uuid"]);
if ($user_data === null)
throw new UnexpectedException("Failed to retrieve user data. Refresh the page and try again.");
throw new UnexpectedException("Failed to retrieve user data. Please refresh the page and try again.");
if ($inputs["email"] === $user_data["email"])
throw new InvalidInputException("That is already your email address.", "email");
throw new InvalidValueException("That is already your email address.", "email");
if ($this->user_list->has_user_with_email($inputs["email"]))
throw new InvalidInputException("That email address is already in use by someone else.", "email");
throw new InvalidValueException("That email address is already in use by someone else.", "email");
$token = $this->user_list->set_email($_SESSION["uuid"], $inputs["email"]);
$this->email_queue->queue_email(new ChangeEmailFromEmail($user_data["email"], $inputs["email"]));

View File

@ -7,12 +7,13 @@ use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\mailer\Email;
use com\fwdekker\deathnotifier\mailer\EmailQueue;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\validator\HasStringLengthRule;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsStringRule;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;
use com\fwdekker\deathnotifier\validation\HasStringLengthRule;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsStringRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
/**
@ -53,14 +54,15 @@ class ChangePasswordAction extends Action
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token, `"password_old": string`: the old
* password, `"password_new": string`: the new password
* @return null
* @throws InvalidInputException if the user is not logged in, if no valid CSRF token is present, if the old
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is not logged in, if no valid CSRF token is present, if the old
* password is incorrect, or if the new password is too short or too long
* @throws UnexpectedException if the current user has been deleted
* @noinspection PhpDocRedundantThrowsInspection can be thrown through {@see TrackingList::transaction()}
*/
public function handle(array $inputs): mixed
{
(new SessionRuleSet(validate_logged_in: true))->check($_SESSION);
(new LoginValidator(validate_logged_in: true))->check($_SESSION);
(new RuleSet([
"token" => [new IsValidCsrfTokenRule()],
"password_old" => [new IsStringRule()],
@ -70,9 +72,9 @@ class ChangePasswordAction extends Action
$this->user_list->transaction(function () use ($inputs) {
$user_data = $this->user_list->get_user_by_uuid($_SESSION["uuid"]);
if ($user_data === null)
throw new UnexpectedException("Failed to retrieve user data. Refresh the page and try again.");
throw new UnexpectedException("Failed to retrieve user data. Please refresh the page and try again.");
if (!password_verify($inputs["password_old"], $user_data["password"]))
throw new InvalidInputException("Incorrect old password.", "password_old");
throw new InvalidValueException("Incorrect old password.", "password_old");
$this->user_list->set_password($_SESSION["uuid"], $inputs["password_new"]);
$this->email_queue->queue_email(new ChangePasswordEmail($user_data["email"]));

View File

@ -4,10 +4,11 @@ namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
/**
@ -40,17 +41,18 @@ class GetPublicUserDataAction extends Action
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token
* @return array{"email": string, "email_verified": bool, "email_notifications_enabled": bool,
* "password_last_change": int} the user's public data
* @throws InvalidInputException if the user is not logged in or if no valid CSRF token is present
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is not logged in or if no valid CSRF token is present
* @throws UnexpectedException if the current user has been deleted
*/
public function handle(array $inputs): array
{
(new SessionRuleSet(validate_logged_in: true))->check($_SESSION);
(new LoginValidator(validate_logged_in: true))->check($_SESSION);
(new RuleSet(["token" => [new IsValidCsrfTokenRule()]]))->check($inputs);
$user_data = $this->user_list->get_user_by_uuid($_SESSION["uuid"]);
if ($user_data === null)
throw new UnexpectedException("Failed to retrieve account data. Refresh the page and try again.");
throw new UnexpectedException("Failed to retrieve account data. Please refresh the page and try again.");
return [
"email" => $user_data["email"],

View File

@ -3,12 +3,13 @@
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
use com\fwdekker\deathnotifier\validator\IsStringRule;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsEmailRule;
use com\fwdekker\deathnotifier\validation\IsStringRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
/**
@ -41,12 +42,13 @@ class LoginAction extends Action
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token, `"email": string`: the email to
* log in with, `"password": string`: the password to log in with
* @return null
* @throws InvalidInputException if the user is logged in, if no account with the given email address exists, if the
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is logged in, if no account with the given email address exists, if the
* password is wrong, or if no valid CSRF token is present
*/
public function handle(array $inputs): mixed
{
(new SessionRuleSet(validate_logged_out: true))->check($_SESSION);
(new LoginValidator(validate_logged_out: true))->check($_SESSION);
(new RuleSet([
"token" => [new IsValidCsrfTokenRule()],
"email" => [new IsEmailRule()],
@ -55,9 +57,9 @@ class LoginAction 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.", "password");
throw new InvalidValueException("No user with that email address has been registered.", "password");
if (!password_verify($inputs["password"], $user_data["password"]))
throw new InvalidInputException("Incorrect password.", "password");
throw new InvalidValueException("Incorrect password.", "password");
$_SESSION["uuid"] = $user_data["uuid"];
return null;

View File

@ -5,10 +5,11 @@ namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\Util;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
use Exception;
@ -24,12 +25,13 @@ class LogoutAction extends Action
*
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token
* @return null
* @throws InvalidInputException if the user is not logged in or if no valid CSRF token is present
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is not logged in or if no valid CSRF token is present
* @throws UnexpectedException if no new CSRF token could be generated
*/
public function handle(array $inputs): mixed
{
(new SessionRuleSet(validate_logged_in: true))->check($_SESSION);
(new LoginValidator(validate_logged_in: true))->check($_SESSION);
(new RuleSet(["token" => [new IsValidCsrfTokenRule()]]))->check($inputs);
session_destroy();

View File

@ -6,12 +6,13 @@ use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\mailer\Email;
use com\fwdekker\deathnotifier\mailer\EmailQueue;
use com\fwdekker\deathnotifier\validator\HasStringLengthRule;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;
use com\fwdekker\deathnotifier\validation\HasStringLengthRule;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsEmailRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
/**
@ -52,12 +53,13 @@ class RegisterAction extends Action
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token, `"email": string`: the email
* address to register the account under, `"password": string`: the password to use for the new account
* @return null
* @throws InvalidInputException if the user is logged in, if no valid CSRF token is present, if the email address
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is logged in, if no valid CSRF token is present, if the email address
* is invalid, if the email address is already in use, or if the password is too short or too long
*/
public function handle(array $inputs): mixed
{
(new SessionRuleSet(validate_logged_out: true))->check($_SESSION);
(new LoginValidator(validate_logged_out: true))->check($_SESSION);
(new RuleSet([
"token" => [new IsValidCsrfTokenRule()],
"email" => [new IsEmailRule()],
@ -66,7 +68,7 @@ class RegisterAction extends Action
$this->user_list->transaction(function () use ($inputs) {
if ($this->user_list->has_user_with_email($inputs["email"]))
throw new InvalidInputException("That email address already in use.", "email");
throw new InvalidValueException("That email address already in use.", "email");
$token = $this->user_list->add_user($inputs["email"], $inputs["password"]);
$this->email_queue->queue_email(new RegisterEmail($inputs["email"], $token));

View File

@ -8,10 +8,11 @@ use com\fwdekker\deathnotifier\mailer\Email;
use com\fwdekker\deathnotifier\mailer\EmailQueue;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\Util;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
/**
@ -51,29 +52,30 @@ class ResendVerifyEmailAction extends Action
*
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token
* @return null
* @throws InvalidInputException if the user is logged out, if no valid CSRF token is present, if the email address
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is logged out, if no valid CSRF token is present, if the email address
* is already verified, or if a verification email was sent too recently
* @throws UnexpectedException if the current user has been deleted
* @noinspection PhpDocRedundantThrowsInspection can be thrown through {@see TrackingList::transaction()}
*/
public function handle(array $inputs): mixed
{
(new SessionRuleSet(validate_logged_in: true))->check($_SESSION);
(new LoginValidator(validate_logged_in: true))->check($_SESSION);
(new RuleSet(["token" => [new IsValidCsrfTokenRule()]]))->check($inputs);
$this->user_list->transaction(function () {
$user_data = $this->user_list->get_user_by_uuid($_SESSION["uuid"]);
if ($user_data === null)
throw new UnexpectedException("Failed to retrieve user data. Refresh the page and try again.");
throw new UnexpectedException("Failed to retrieve user data. Please refresh the page and try again.");
if ($user_data["email_verification_token"] === null)
throw new InvalidInputException("Your email address is already verified.");
throw new InvalidValueException("Your email address is already verified.");
$minutes_left = Util::minutes_until_interval_elapsed(
$user_data["email_verification_token_timestamp"],
UserList::MINUTES_BETWEEN_VERIFICATION_EMAILS
);
if ($minutes_left > 0) {
throw new InvalidInputException(
throw new InvalidValueException(
"A verification email was sent recently. " .
"Please wait $minutes_left more minute(s) before requesting a new email."
);

View File

@ -7,12 +7,13 @@ use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\IllegalStateError;
use com\fwdekker\deathnotifier\mailer\Email;
use com\fwdekker\deathnotifier\mailer\EmailQueue;
use com\fwdekker\deathnotifier\validator\HasStringLengthRule;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
use com\fwdekker\deathnotifier\validator\IsStringRule;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validation\HasStringLengthRule;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsEmailRule;
use com\fwdekker\deathnotifier\validation\IsStringRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
/**
@ -54,7 +55,8 @@ class ResetPasswordAction extends Action
* address of the account to reset the password of, `"password": string`: the new password to set,
* `"reset_token": string`: the token to reset the password with
* @return null
* @throws InvalidInputException if no valid CSRF token is present, if no account with the given email address
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if no valid CSRF token is present, if no account with the given email address
* exists, if the password is too short or too long, if the reset token is invalid, or if the reset token has
* expired
*/

View File

@ -7,10 +7,11 @@ use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\mailer\Email;
use com\fwdekker\deathnotifier\mailer\EmailQueue;
use com\fwdekker\deathnotifier\Util;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsEmailRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
/**
@ -51,7 +52,8 @@ class SendPasswordResetAction extends Action
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token, `"email": string`: the email
* address of the account to send a password reset email for
* @return null
* @throws InvalidInputException if no valid CSRF token is present, if no account with the given email address
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if no valid CSRF token is present, if no account with the given email address
* exists, or if a password reset email was sent too recently
*/
public function handle(array $inputs): mixed
@ -64,14 +66,14 @@ class SendPasswordResetAction extends Action
$this->user_list->transaction(function () use ($inputs) {
$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.");
throw new InvalidValueException("No user with that email address has been registered.");
$minutes_left = Util::minutes_until_interval_elapsed(
$user_data["password_reset_token_timestamp"],
UserList::MINUTES_BETWEEN_PASSWORD_RESETS
);
if ($minutes_left > 0) {
throw new InvalidInputException(
throw new InvalidValueException(
"A password reset email was sent recently. " .
"Please wait $minutes_left more minute(s) before requesting a new email."
);

View File

@ -4,11 +4,12 @@ namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsBooleanRule;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsBooleanRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
/**
@ -41,14 +42,15 @@ class ToggleNotificationsAction extends Action
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token, `"enable_notifications": bool`:
* `true` if and only if notifications should be enabled
* @return null
* @throws InvalidInputException if the user is not logged in, if no valid CSRF token is present, if the toggle
* value is not a boolean, or if the user's email address it not verified
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is not logged in, if no valid CSRF token is present, or if the user's
* email address is not verified
* @throws UnexpectedException if the current user has been deleted
* @noinspection PhpDocRedundantThrowsInspection can be thrown through {@see TrackingList::transaction()}
*/
public function handle(array $inputs): mixed
{
(new SessionRuleSet(validate_logged_in: true))->check($_SESSION);
(new LoginValidator(validate_logged_in: true))->check($_SESSION);
(new RuleSet([
"token" => [new IsValidCsrfTokenRule()],
"enable_notifications" => [new IsBooleanRule()],
@ -57,9 +59,9 @@ class ToggleNotificationsAction extends Action
$this->user_list->transaction(function () use ($inputs) {
$user_data = $this->user_list->get_user_by_uuid($_SESSION["uuid"]);
if ($user_data === null)
throw new UnexpectedException("Failed to retrieve user data. Refresh the page and try again.");
throw new UnexpectedException("Failed to retrieve user data. Please refresh the page and try again.");
if ($inputs["enable_notifications"] && $user_data["email_verification_token"] !== null)
throw new InvalidInputException("Please verify your email address before toggling notifications.");
throw new InvalidValueException("Please verify your email address before toggling notifications.");
$this->user_list->set_notifications_enabled($_SESSION["uuid"], $inputs["enable_notifications"]);
});

View File

@ -5,10 +5,11 @@ namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\Util;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validator\SessionRuleSet;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\validation\LoginValidator;
use Exception;
@ -41,12 +42,13 @@ class UserDeleteAction extends Action
*
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token
* @return null
* @throws InvalidInputException if the user is not logged in or if no valid CSRF token is present
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the user is not logged in or if no valid CSRF token is present
* @throws UnexpectedException if no new CSRF token could be generated
*/
public function handle(array $inputs): mixed
{
(new SessionRuleSet(validate_logged_in: true))->check($_SESSION);
(new LoginValidator(validate_logged_in: true))->check($_SESSION);
(new RuleSet(["token" => [new IsValidCsrfTokenRule()]]))->check($inputs);
$this->user_list->remove_user_by_uuid($_SESSION["uuid"]);

View File

@ -4,11 +4,12 @@ namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\Util;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
use com\fwdekker\deathnotifier\validator\IsStringRule;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsEmailRule;
use com\fwdekker\deathnotifier\validation\IsStringRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
/**
@ -41,7 +42,8 @@ class ValidatePasswordResetTokenAction extends Action
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token, `"email"`: the email address to
* validate the password reset token of, `"reset_token": string`: the password reset token to validate
* @return null
* @throws InvalidInputException if no valid CSRF token is present, if no account with the given email address
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if no valid CSRF token is present, if no account with the given email address
* exists, if the reset token is invalid, or if the reset token has expired
*/
public function handle(array $inputs): mixed
@ -63,16 +65,16 @@ class ValidatePasswordResetTokenAction extends Action
* @param string $email the email address to validate the password reset token of
* @param string $reset_token the password reset token to validate
* @return void
* @throws InvalidInputException if no account with the given email address exists, if the reset token is invalid,
* @throws InvalidValueException if no account with the given email address exists, if the reset token is invalid,
* or if the reset token has expired
*/
public static function validate_token(UserList $user_list, string $email, string $reset_token): void
{
$user_data = $user_list->get_user_by_email($email);
if ($user_data === null)
throw new InvalidInputException("No user with that email address has been registered.");
throw new InvalidValueException("No user with that email address has been registered.");
if ($reset_token !== $user_data["password_reset_token"])
throw new InvalidInputException(
throw new InvalidValueException(
"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. " .
@ -84,7 +86,7 @@ class ValidatePasswordResetTokenAction extends Action
UserList::MINUTES_VALID_PASSWORD_RESET
);
if ($minutes_left < 0) {
throw new InvalidInputException(
throw new InvalidValueException(
`This password reset link has expired. <a href="./">Return to the front page</a> and press the ` .
`"Forgot password?" button to request a new password reset link.`
);

View File

@ -4,11 +4,12 @@ namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\Util;
use com\fwdekker\deathnotifier\validator\InvalidInputException;
use com\fwdekker\deathnotifier\validator\IsEmailRule;
use com\fwdekker\deathnotifier\validator\IsStringRule;
use com\fwdekker\deathnotifier\validator\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validator\RuleSet;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsEmailRule;
use com\fwdekker\deathnotifier\validation\IsStringRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
/**
@ -41,7 +42,8 @@ class VerifyEmailAction extends Action
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token, `"email"`: the email address to
* verify, `"verify_token": string`: the token to verify the email address with
* @return null
* @throws InvalidInputException if no valid CSRF token is present, if no account with the given email address
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if no valid CSRF token is present, if no account with the given email address
* exists, if the reset token is invalid, or if the reset token has expired
*/
public function handle(array $inputs): mixed
@ -55,11 +57,11 @@ class VerifyEmailAction extends Action
$this->user_list->transaction(function () use ($inputs) {
$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.");
throw new InvalidValueException("No user with that email address has been registered.");
if ($user_data["email_verification_token"] === null)
throw new InvalidInputException("Your email address is already verified. You can close this page.");
throw new InvalidValueException("Your email address is already verified. You can close this page.");
if ($inputs["verify_token"] !== $user_data["email_verification_token"])
throw new InvalidInputException(
throw new InvalidValueException(
"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. " .
@ -71,7 +73,7 @@ class VerifyEmailAction extends Action
UserList::MINUTES_VALID_VERIFICATION
);
if ($minutes_left < 0)
throw new InvalidInputException(
throw new InvalidValueException(
"This email verification link has expired. Log in and request a new verification email."
);

View File

@ -1,9 +1,9 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
namespace com\fwdekker\deathnotifier\validation;
use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\IllegalArgumentError;
use com\fwdekker\deathnotifier\IllegalStateError;
/**
@ -27,22 +27,21 @@ class EqualsCliPasswordRule extends Rule
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
* @param string $key the key in {@see $inputs} of the input to check
* @return void if the checked input equals the CLI password
* @throws InvalidInputException if the checked input does not equal the CLI password
* @throws IllegalArgumentError if the CLI password is a blank string, if the CLI password is at its default value,
* if the checked input is not set
* @throws InvalidTypeException if the CLI password is a blank string, if the CLI password is at its default
* value, or if the checked input is not set
* @throws InvalidValueException if the checked input does not equal the CLI password
*/
public function check(array $inputs, string $key): void
{
if (!Config::has(self::CONFIG_KEY) || trim(Config::get(self::CONFIG_KEY)) === "")
throw new IllegalArgumentError("The CLI is disabled because the CLI password is not set.");
throw new IllegalStateError("The CLI is disabled because the CLI password is not set.");
if (Config::get(self::CONFIG_KEY) === self::DEFAULT)
throw new IllegalArgumentError("The CLI is disabled because the CLI password is set to the default.");
throw new IllegalStateError("The CLI is disabled because the CLI password is set to the default.");
if (!isset($inputs[$key]))
throw new InvalidInputException("This operation requires the CLI password.", $key);
throw new InvalidTypeException("This operation requires the CLI password.");
if (!password_verify($inputs[$key], Config::get(self::CONFIG_KEY)))
throw new InvalidInputException("Incorrect CLI password.", $key);
throw new InvalidValueException("Incorrect CLI password.", $key);
}
}

View File

@ -1,6 +1,8 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
namespace com\fwdekker\deathnotifier\validation;
use InvalidArgumentException;
/**
@ -26,6 +28,11 @@ class HasStringLengthRule extends Rule
*/
public function __construct(?int $min_length = null, ?int $max_length = null)
{
if ($min_length !== null && $max_length !== null && $min_length > $max_length)
throw new InvalidArgumentException("Minimum length should not exceed maximum length.");
if ($min_length === null && $max_length === null)
throw new InvalidArgumentException("Either minimum length or maximum length should be non-null.");
$this->min_length = $min_length;
$this->max_length = $max_length;
}
@ -37,18 +44,23 @@ class HasStringLengthRule extends Rule
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
* @param string $key the key in {@see $inputs} of the input to check
* @return void if the checked input is of the specified length
* @throws InvalidInputException if the checked input is not set or is not of the specified length
* @throws InvalidTypeException if the checked input is not set or is not a string
* @throws InvalidValueException if the checked input is not of the specified length
*/
public function check(array $inputs, string $key): void
{
if (!isset($inputs[$key]))
throw new InvalidInputException("Missing input '$key'.", $key);
throw new InvalidTypeException("Required input '$key' not set.");
if (!is_string($inputs[$key]))
throw new InvalidInputException("Field must be a string.", $key);
throw new InvalidTypeException("Input '$key' should be string, but is " . gettype($inputs[$key]) . ".");
if ($this->min_length !== null && strlen($inputs[$key]) < $this->min_length)
throw new InvalidInputException("Use at least $this->min_length character(s).", $key);
if ($this->max_length !== null && strlen($inputs[$key]) > $this->max_length)
throw new InvalidInputException("Use at most $this->max_length character(s).", $key);
if ($this->min_length !== null && strlen($inputs[$key]) < $this->min_length) {
$characters = $this->min_length !== 1 ? "characters" : "character";
throw new InvalidValueException("Use at least $this->min_length $characters.", $key);
}
if ($this->max_length !== null && strlen($inputs[$key]) > $this->max_length) {
$characters = $this->min_length !== 1 ? "characters" : "character";
throw new InvalidValueException("Use at most $this->max_length $characters.", $key);
}
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace com\fwdekker\deathnotifier\validation;
use com\fwdekker\deathnotifier\MalformedRequestException;
/**
* Thrown to indicate that a request to the server is missing a required input or has an input of the incorrect type.
*
* If the input is set and of the correct type, but is invalid for another reason, throw {@see InvalidValueException}
* instead.
*/
class InvalidTypeException extends MalformedRequestException
{
// Intentionally left empty
}

View File

@ -1,6 +1,6 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
namespace com\fwdekker\deathnotifier\validation;
use com\fwdekker\deathnotifier\MalformedRequestException;
use Throwable;
@ -8,8 +8,10 @@ use Throwable;
/**
* Thrown to indicate that a request to the server contains an invalid input.
*
* If the input is not set or is of the incorrect type, throw {@see InvalidTypeException} instead.
*/
class InvalidInputException extends MalformedRequestException
class InvalidValueException extends MalformedRequestException
{
/**
* @var string|null the input element that caused the exception, or `null` if no such element could be identified
@ -18,7 +20,7 @@ class InvalidInputException extends MalformedRequestException
/**
* Constructs a new `InvalidInputException`.
* Constructs a new `InvalidValueException`.
*
* @param string $message the message to show to the user
* @param string|null $target the input that caused the exception, or `null` if no such element could be identified

View File

@ -1,6 +1,6 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
namespace com\fwdekker\deathnotifier\validation;
/**
@ -14,11 +14,13 @@ class IsBooleanRule extends Rule
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
* @param string $key the key in {@see $inputs} of the input to check
* @return void if the checked input is a boolean
* @throws InvalidInputException if the checked input is not a boolean
* @throws InvalidTypeException if the checked input is not set or if the checked input is not a boolean
*/
public function check(array $inputs, string $key): void
{
if (!isset($inputs[$key]) || !is_bool($inputs[$key]))
throw new InvalidInputException("Field '" . htmlentities($key) . "' must be a boolean.", $key);
if (!isset($inputs[$key]))
throw new InvalidTypeException("Required input '$key' not set.");
if (!is_bool($inputs[$key]))
throw new InvalidTypeException("Input '$key' should be boolean, but is " . gettype($inputs[$key]) . ".");
}
}

View File

@ -1,8 +1,6 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\IllegalArgumentError;
namespace com\fwdekker\deathnotifier\validation;
/**
@ -16,28 +14,28 @@ class IsEmailRule extends Rule
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
* @param string $key the key in {@see $inputs} of the input to check
* @return void if the checked input is an email address
* @throws InvalidInputException if the checked input is not an email address
* @throws IllegalArgumentError if the checked input is not set or is not a string
* @throws InvalidTypeException if the checked input is not set or is not a string
* @throws InvalidValueException if the checked input is not an email address
*/
public function check(array $inputs, string $key): void
{
if (!isset($inputs[$key]))
throw new IllegalArgumentError("Required input '$key' not set.");
throw new InvalidTypeException("Required input '$key' not set.");
if (!is_string($inputs[$key]))
throw new IllegalArgumentError("Input '$key' should be string, but is " . gettype($inputs[$key]) . ".");
throw new InvalidTypeException("Input '$key' should be string, but is " . gettype($inputs[$key]) . ".");
$input = $inputs[$key];
if (str_starts_with($input, " "))
throw new InvalidInputException("Remove the spaces at the start.", $key);
throw new InvalidValueException("Remove the spaces at the start.", $key);
if (str_ends_with($input, " "))
throw new InvalidInputException("Remove the spaces at the end.", $key);
throw new InvalidValueException("Remove the spaces at the end.", $key);
if ($input === "")
throw new InvalidInputException("Enter an email address.", $key);
throw new InvalidValueException("Enter an email address.", $key);
if (!str_contains($input, "@"))
throw new InvalidInputException("Don't forget to add an '@' symbol.", $key);
throw new InvalidValueException("Don't forget to add an '@' symbol.", $key);
if (str_ends_with($input, "@"))
throw new InvalidInputException("Add a domain name after the '@'.", $key);
throw new InvalidValueException("Add a domain name after the '@'.", $key);
if (!filter_var($input, FILTER_VALIDATE_EMAIL))
throw new InvalidInputException("Enter a valid email address.", $key);
throw new InvalidValueException("Enter a valid email address.", $key);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace com\fwdekker\deathnotifier\validation;
/**
* Validates that the input is not a blank string.
*/
class IsNotBlankRule extends Rule
{
/**
* Validates that the input is not a blank string.
*
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
* @param string $key the key in {@see $inputs} of the input to check
* @return void if the checked input is not a blank string
* @throws InvalidTypeException if the checked input is not set or is not a string
* @throws InvalidValueException if the checked input is a blank string
*/
public function check(array $inputs, string $key): void
{
if (!isset($inputs[$key]))
throw new InvalidTypeException("Required input '$key' not set.");
if (!is_string($inputs[$key]))
throw new InvalidTypeException("Input '$key' should be string, but is " . gettype($inputs[$key]) . ".");
if ($inputs[$key] === "")
throw new InvalidValueException("Use at least one character.", $key);
if (trim($inputs[$key]) === "")
throw new InvalidValueException("Use at least one character other than a space.", $key);
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
namespace com\fwdekker\deathnotifier\validation;
/**
@ -14,11 +14,13 @@ class IsStringRule extends Rule
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
* @param string $key the key in {@see $inputs} of the input to check
* @return void if the checked input is a string
* @throws InvalidInputException if the checked input is not a string
* @throws InvalidTypeException if the checked input is not set or if the checked input is not a string
*/
public function check(array $inputs, string $key): void
{
if (!isset($inputs[$key]) || !is_string($inputs[$key]))
throw new InvalidInputException("Field '" . htmlentities($key) . "' must be a string.", $key);
if (!isset($inputs[$key]))
throw new InvalidTypeException("Required input '$key' not set.");
if (!is_string($inputs[$key]))
throw new InvalidTypeException("Input '$key' should be string, but is " . gettype($inputs[$key]) . ".");
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace com\fwdekker\deathnotifier\validation;
/**
* Validates that the input is a valid CSRF token.
*/
class IsValidCsrfTokenRule extends Rule
{
/**
* Validates that the input is a valid CSRF token.
*
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
* @param string $key the key in {@see $inputs} of the input to check
* @return void if the checked input is a valid CSRF token
* @throws InvalidTypeException if the checked input is not set or if the checked input is not a string
* @throws InvalidValueException if the checked input is not a valid CSRF token
*/
public function check(array $inputs, string $key): void
{
if (!isset($inputs[$key]))
throw new InvalidTypeException("Missing CSRF token.");
if (!is_string($inputs[$key]))
throw new InvalidTypeException("CSRF token should be string, but is " . gettype($inputs[$key]) . ".");
if ($inputs[$key] !== $_SESSION["token"])
throw new InvalidValueException("Invalid CSRF token. Please refresh the page and try again.", $key);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace com\fwdekker\deathnotifier\validation;
use com\fwdekker\deathnotifier\IllegalArgumentError;
/**
* Like a {@see RuleSet}, but specifically to validate whether the user is logged in.
*/
class LoginValidator
{
/**
* @var bool `true` if and only if this validator should check if the user is logged in
*/
private readonly bool $validate_logged_in;
/**
* @var bool `true` if and only if this validator should check if the user is logged out
*/
private readonly bool $validate_logged_out;
/**
* Constructs a new `LoginValidator`.
*
* @param bool $validate_logged_in `true` if and only if this validator should check if the user is logged in
* @param bool $validate_logged_out `true` if and only if this validator should check if the user is logged out
*/
public function __construct(bool $validate_logged_in = false, bool $validate_logged_out = false)
{
if ($validate_logged_in && $validate_logged_out)
throw new IllegalArgumentError("Cannot require that user is both logged in and logged out.");
if (!$validate_logged_in && !$validate_logged_out)
throw new IllegalArgumentError("Must require that user is either logged in or logged out.");
$this->validate_logged_in = $validate_logged_in;
$this->validate_logged_out = $validate_logged_out;
}
/**
* Checks whether the user's login status is as desired.
*
* @param array<int|string, mixed> $inputs the list of inputs to validate
* @return void if the user's login status is as desired
* @throws InvalidValueException if the user's login status is not as desired
*/
public function check(array $inputs): void
{
if ($this->validate_logged_in && !isset($inputs["uuid"]))
throw new InvalidValueException(
"You must be logged in to perform this action. Please refresh the page and try again."
);
if ($this->validate_logged_out && isset($inputs["uuid"]))
throw new InvalidValueException(
"You must be logged out to perform this action. Please refresh the page and try again."
);
}
}

View File

@ -1,8 +1,6 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\IllegalArgumentError;
namespace com\fwdekker\deathnotifier\validation;
/**
@ -16,8 +14,8 @@ abstract class Rule
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
* @param string $key the key in {@see $inputs} of the input to check
* @return void if the rule holds for the checked input
* @throws InvalidInputException if the rule does not hold for the checked input
* @throws IllegalArgumentError if the checked input is not set or of the wrong type
* @throws InvalidTypeException if the checked input is not set or of the wrong type
* @throws InvalidValueException if the rule does not hold for the checked input
*/
public abstract function check(array $inputs, string $key): void;
}

View File

@ -0,0 +1,43 @@
<?php
namespace com\fwdekker\deathnotifier\validation;
/**
* A set of {@see Rule Rules} to apply to an array of inputs.
*/
class RuleSet
{
/**
* @var array<string, array<Rule>> a mapping from input keys to the array of rules to apply to that input
*/
private readonly array $rule_lists;
/**
* Constructs a new `RuleSet`.
*
* @param array<string, array<Rule>> $rule_lists a mapping from input keys to the array of rules to apply to that
* input
*/
public function __construct(array $rule_lists)
{
$this->rule_lists = $rule_lists;
}
/**
* Verifies that the input is of the specific length.
*
* @param array<int|string, mixed> $inputs the list of inputs to validate
* @return void if the inputs satisfy the rules of this rule set
* @throws InvalidTypeException if any rule throws this exception for any input
* @throws InvalidValueException if any rule throws this exception for any input
*/
public function check(array $inputs): void
{
foreach ($this->rule_lists as $key => $rule_list)
foreach ($rule_list as $rule)
$rule->check($inputs, $key);
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
/**
* Validates that the input is not a blank string.
*/
class IsNotBlankRule extends Rule
{
/**
* Validates that the input is not a blank string.
*
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
* @param string $key the key in {@see $inputs} of the input to check
* @return void if the checked input is not a blank string
* @throws InvalidInputException if the checked input is not set, is not a string, or is a blank string
*/
public function check(array $inputs, string $key): void
{
if (!isset($inputs[$key]) || !is_string($inputs[$key]) || trim($inputs[$key]) === "")
throw new InvalidInputException("Use at least one character.", $key);
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
/**
* Validates that the input is not set.
*/
class IsNotSetRule extends Rule
{
/**
* @var string|null the message to return if the input is not set, or `null` to show the default message
*/
public readonly ?string $override_message;
/**
* Instantiates a new `IsSetRule`.
*
* @param string|null $override_message the message to return if the input is not set, or `null` to show the default
* message
*/
public function __construct(?string $override_message = null)
{
$this->override_message = $override_message;
}
/**
* Validates that the input is not set.
*
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
* @param string $key the key in {@see $inputs} of the input to check
* @return void if the checked input is not set
* @throws InvalidInputException if the checked input is set
*/
public function check(array $inputs, string $key): void
{
if (isset($inputs[$key]))
throw new InvalidInputException(
$this->override_message ?? "Field '" . htmlentities($key) . "' must not be set.",
$key
);
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
/**
* Validates that the input is set.
*/
class IsSetRule extends Rule
{
/**
* @var string|null the message to return if the input is not set, or `null` to show the default message
*/
public readonly ?string $override_message;
/**
* Instantiates a new `IsSetRule`.
*
* @param string|null $override_message the message to return if the input is not set, or `null` to show the default
* message
*/
public function __construct(?string $override_message = null)
{
$this->override_message = $override_message;
}
/**
* Validates that the input is set.
*
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
* @param string $key the key in {@see $inputs} of the input to check
* @return void if the checked input is set
* @throws InvalidInputException if the checked input is not set
*/
public function check(array $inputs, string $key): void
{
if (!isset($inputs[$key]))
throw new InvalidInputException(
$this->override_message ?? "Field '" . htmlentities($key) . "' must be set.",
$key
);
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
/**
* Validates that the input is a valid CSRF token.
*/
class IsValidCsrfTokenRule extends Rule
{
/**
* Validates that the input is a valid CSRF token.
*
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
* @param string $key the key in {@see $inputs} of the input to check
* @return void if the checked input is a valid CSRF token
* @throws InvalidInputException if the checked input is not a valid CSRF token
*/
public function check(array $inputs, string $key): void
{
if (!isset($inputs[$key]) || $inputs[$key] !== $_SESSION["token"])
throw new InvalidInputException("Invalid request token. Please refresh the page and try again.", $key);
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
/**
* A set of {@see Rule Rules} to apply to an array of inputs.
*/
class RuleSet
{
/**
* @var array<string, array<Rule>> the rules to apply to the inputs
*/
private readonly array $rule_set;
/**
* Constructs a new `RuleSet`.
*
* @param array<string, array<Rule>> $rules the rules to apply to the inputs
*/
public function __construct(array $rules)
{
$this->rule_set = $rules;
}
/**
* Verifies that the input is of the specific length.
*
* @param array<int|string, mixed> $inputs the list of inputs to validate
* @return void if the inputs satisfy the rules of this rule set
* @throws InvalidInputException if any input does not satisfy any rule of this rule set
*/
public function check(array $inputs): void
{
foreach ($this->rule_set as $key => $rules)
foreach ($rules as $rule)
$rule->check($inputs, $key);
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\IllegalArgumentError;
/**
* A {@see RuleSet} specifically to validate the current session.
*/
class SessionRuleSet extends RuleSet
{
/**
* Constructs a new `RuleSet`.
*
* @param bool $validate_logged_in `true` if and only if this rule set should validate that the user is logged in
* @param bool $validate_logged_out `true` if and only if this rule set should validate that the user is logged out
*/
public function __construct(bool $validate_logged_in = false,
bool $validate_logged_out = false)
{
if ($validate_logged_in && $validate_logged_out)
throw new IllegalArgumentError("Cannot require that user is both logged in and logged out.");
$rules = [];
if ($validate_logged_in)
$rules["uuid"] =
[new IsSetRule("You must be logged in to perform this action. Refresh the page and try again.")];
if ($validate_logged_out)
$rules["uuid"] =
[new IsNotSetRule("You must be logged out to perform this action. Refresh the page and try again.")];
parent::__construct($rules);
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
namespace com\fwdekker\deathnotifier\validation;
use com\fwdekker\deathnotifier\Config;
use PHPUnit\Framework\TestCase;
@ -91,6 +91,16 @@ class ConfigTest extends TestCase
self::assertEquals("value", Config::get("test_section.property"));
}
public function test_get_returns_null_if_section_does_not_exist(): void
{
self::assertNull(Config::get("test_section"));
}
public function test_get_returns_null_if_property_does_not_exist(): void
{
self::assertNull(Config::get("test_section.property"));
}
public function test_reset(): void
{

View File

@ -1,9 +1,10 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
namespace com\fwdekker\deathnotifier\validation;
use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\IllegalArgumentError;
use com\fwdekker\deathnotifier\IllegalStateError;
use com\fwdekker\deathnotifier\MalformedRequestException;
use PHPUnit\Framework\TestCase;
use Throwable;
@ -16,20 +17,16 @@ class EqualsCliPasswordRuleTest extends TestCase
/**
* Tests the output of {@see EqualsCliPasswordRule::check()}.
*
* @param string $name the name to give to the test case
* @param string|null $password the password that is set in the configuration
* @param string|null $input the user's input
* @param class-string<Throwable>|null $exception the exception that is asserted to be thrown
* @param string|null $exception_message the exception message that is asserted
* @return void
* @throws InvalidInputException if {@see $exception} is `null` but an exception is thrown
* @throws MalformedRequestException never
* @dataProvider check_provider
*/
public function test_check(string $name, ?string $password, ?string $input, ?string $exception,
?string $exception_message): void
public function test_check(?string $password, ?string $input, ?string $exception, ?string $exception_message): void
{
self::setName($name);
if ($exception !== null)
self::expectException($exception);
if ($exception_message !== null)
@ -46,23 +43,24 @@ class EqualsCliPasswordRuleTest extends TestCase
/**
* Returns the test cases.
*
* @return array<array{string, string|null, string|null, class-string<Throwable>|null, string|null}> the test cases
* @return array<string, array{string|null, string|null, class-string<Throwable>|null, string|null}> the test cases
* @see RuleTest::test_check()
*/
public function check_provider(): array
{
$hash = "\$2y\$04\$fwXTw7Rjzw0EpU094u4agOBaBNqtCHGc4TMoxfbPrxuqO5tpYyRka"; # Hash of "password"
$error = IllegalArgumentError::class;
$exception = InvalidInputException::class;
$error = IllegalStateError::class;
$type = InvalidTypeException::class;
$value = InvalidValueException::class;
return [
["error if password is not set", null, "input", $error, "The CLI is disabled because the CLI password is not set."],
["error if password is blank", " ", "input", $error, "The CLI is disabled because the CLI password is not set."],
["error if password is default", EqualsCliPasswordRule::DEFAULT, "input", $error, "The CLI is disabled because the CLI password is set to the default."],
["exception if input is not set", $hash, null, $exception, "This operation requires the CLI password."],
["exception if input is incorrect", $hash, "incorrect", $exception, "Incorrect CLI password."],
["no exception if input is correct", $hash, "password", null, null],
"error if password is not set" => [null, "input", $error, "The CLI is disabled because the CLI password is not set."],
"error if password is blank" => [" ", "input", $error, "The CLI is disabled because the CLI password is not set."],
"error if password is default" => [EqualsCliPasswordRule::DEFAULT, "input", $error, "The CLI is disabled because the CLI password is set to the default."],
"exception if input is not set" => [$hash, null, $type, "This operation requires the CLI password."],
"exception if input is incorrect" => [$hash, "incorrect", $value, "Incorrect CLI password."],
"no exception if input is correct" => [$hash, "password", null, null],
];
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace com\fwdekker\deathnotifier\validation;
use InvalidArgumentException;
/**
* Unit tests for {@see HasStringLengthRule}.
*/
class HasStringLengthRuleTest extends RuleTest
{
public function check_provider(): array
{
$type = InvalidTypeException::class;
$value = InvalidValueException::class;
return [
"exception if input is not set" => [new HasStringLengthRule(2, 3), null, $type, "Required input 'key' not set."],
"exception if input is not a string" => [new HasStringLengthRule(2, 3), 221, $type, "Input 'key' should be string, but is integer."],
"exception if input is too short" => [new HasStringLengthRule(3, 4), "a", $value, "Use at least 3 characters."],
"exception if input is too long" => [new HasStringLengthRule(4, 6), "long-input", $value, "Use at most 6 characters."],
"exception if input is too short and there is no maximum" => [new HasStringLengthRule(3, null), "a", $value, "Use at least 3 characters."],
"exception if input is too long and there is no minimum" => [new HasStringLengthRule(null, 6), "long-input", $value, "Use at most 6 characters."],
"no exception if input is minimum length" => [new HasStringLengthRule(2, 5), "je", null, null],
"no exception if input is maximum length" => [new HasStringLengthRule(3, 4), "word", null, null],
"no exception if input is within boundaries" => [new HasStringLengthRule(4, 7), "string", null, null],
"no exception if input is empty and there is no minimum" => [new HasStringLengthRule(null, 4), "te", null, null],
"no exception if input is long and there is no maximum" => [new HasStringLengthRule(2, null), "a-very-long-string", null, null],
];
}
public function test_constructor_throws_error_if_both_limits_are_null(): void
{
self::expectException(InvalidArgumentException::class);
new HasStringLengthRule();
}
public function test_constructor_throws_error_if_minimum_exceeds_maximum(): void
{
self::expectException(InvalidArgumentException::class);
new HasStringLengthRule(4, 2);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace com\fwdekker\deathnotifier\validation;
/**
* Unit tests for {@see IsBooleanRule}.
*/
class IsBooleanRuleTest extends RuleTest
{
public function check_provider(): array
{
$type = InvalidTypeException::class;
return [
"exception if input is not set" => [new IsBooleanRule(), null, $type, "Required input 'key' not set."],
"exception if input is integer" => [new IsBooleanRule(), 1, $type, "Input 'key' should be boolean, but is integer."],
"exception if input is string" => [new IsBooleanRule(), "true", $type, "Input 'key' should be boolean, but is string."],
"no exception if input is `true`" => [new IsBooleanRule(), true, null, null],
"no exception if input is `false`" => [new IsBooleanRule(), false, null, null],
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace com\fwdekker\deathnotifier\validation;
/**
* Unit tests for {@see IsEmailRule}.
*/
class IsEmailRuleTest extends RuleTest
{
public function check_provider(): array
{
$type = InvalidTypeException::class;
$value = InvalidValueException::class;
return [
"exception if input is not set" => [new IsEmailRule(), null, $type, "Required input 'key' not set."],
"exception if input is not a string" => [new IsEmailRule(), 491, $type, "Input 'key' should be string, but is integer."],
"exception if input starts with space" => [new IsEmailRule(), " valid@example.com", $value, "Remove the spaces at the start."],
"exception if input ends with space" => [new IsEmailRule(), "valid@example.com ", $value, "Remove the spaces at the end."],
"exception if input is empty" => [new IsEmailRule(), "", $value, "Enter an email address."],
"exception if input misses the '@' symbol" => [new IsEmailRule(), "invalid", $value, "Don't forget to add an '@' symbol."],
"exception if input misses the domain part" => [new IsEmailRule(), "invalid@", $value, "Add a domain name after the '@'."],
"exception if input is invalid in a complex way" => [new IsEmailRule(), "invalid@example", $value, "Enter a valid email address."],
"no exception if input is email" => [new IsEmailRule(), "valid@example.com", null, null],
];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace com\fwdekker\deathnotifier\validation;
/**
* Unit tests for {@see IsNotBlankRule}.
*/
class IsNotBlankRuleTest extends RuleTest
{
public function check_provider(): array
{
$type = InvalidTypeException::class;
$value = InvalidValueException::class;
return [
"exception if input is not set" => [new IsNotBlankRule(), null, $type, "Required input 'key' not set."],
"exception if input is not a string" => [new IsNotBlankRule(), 621, $type, "Input 'key' should be string, but is integer."],
"exception if input is empty" => [new IsNotBlankRule(), "", $value, "Use at least one character."],
"exception if input is whitespace only" => [new IsNotBlankRule(), " ", $value, "Use at least one character other than a space."],
"no exception if input is not blank" => [new IsNotBlankRule(), "not-blank", null, null],
"no exception if input is not blank but starts with whitespace" => [new IsNotBlankRule(), " not-blank ", null, null],
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace com\fwdekker\deathnotifier\validation;
/**
* Unit tests for {@see IsStringRule}.
*/
class IsStringRuleTest extends RuleTest
{
public function check_provider(): array
{
$type = InvalidTypeException::class;
return [
"exception if input is not set" => [new IsStringRule(), null, $type, "Required input 'key' not set."],
"exception if input is not a string" => [new IsStringRule(), 47, $type, "Input 'key' should be string, but is integer."],
"no exception if input is the empty string" => [new IsStringRule(), "", null, null],
"no exception if input is a blank string" => [new IsStringRule(), " ", null, null],
"no exception if input is a string" => [new IsStringRule(), "not-empty", null, null],
];
}
}

View File

@ -0,0 +1,44 @@
<?php /** @noinspection PhpUnhandledExceptionInspection */
namespace com\fwdekker\deathnotifier\validation;
/**
* Unit tests for {@see IsValidCsrfTokenRule}.
*/
class IsValidCsrfTokenRuleTest extends RuleTest
{
public function check_provider(): array
{
$type = InvalidTypeException::class;
return [
"exception if input is not set" => [new IsValidCsrfTokenRule(), null, $type, "Required input 'key' not set."],
"exception if input is not a string" => [new IsValidCsrfTokenRule(), 170, $type, "Input 'key' should be string, but is integer."],
];
}
protected function setUp(): void
{
$_SESSION = [];
}
public function test_exception_if_input_is_incorrect_token(): void
{
$_SESSION["token"] = "valid";
self::expectException(InvalidValueException::class);
(new IsValidCsrfTokenRule())->check(["key" => "invalid"], "key");
}
public function test_no_exception_if_input_is_correct_token(): void
{
$_SESSION["token"] = "valid";
(new IsValidCsrfTokenRule())->check(["key" => "valid"], "key");
self::assertTrue(true);
}
}

View File

@ -0,0 +1,56 @@
<?php /** @noinspection PhpUnhandledExceptionInspection */
namespace com\fwdekker\deathnotifier\validation;
use com\fwdekker\deathnotifier\IllegalArgumentError;
use PHPUnit\Framework\TestCase;
/**
* Unit tests for {@see LoginValidator}.
*/
class LoginValidatorTest extends TestCase
{
public function test_constructor_throws_exception_if_user_should_be_both_logged_in_and_logged_out(): void
{
self::expectException(IllegalArgumentError::class);
new LoginValidator(validate_logged_in: true, validate_logged_out: true);
}
public function test_constructor_throws_exception_if_user_should_be_neither_logged_in_nor_logged_out(): void
{
self::expectException(IllegalArgumentError::class);
new LoginValidator(validate_logged_in: false, validate_logged_out: false);
}
public function test_check_throws_exception_if_log_in_required_but_not_logged_in(): void
{
self::expectException(InvalidValueException::class);
(new LoginValidator(validate_logged_in: true))->check([]);
}
public function test_check_no_exception_if_log_in_required_and_logged_in(): void
{
(new LoginValidator(validate_logged_in: true))->check(["uuid" => "my-uuid"]);
self::assertTrue(true);
}
public function test_check_throws_exception_if_log_out_required_but_not_logged_out(): void
{
self::expectException(InvalidValueException::class);
(new LoginValidator(validate_logged_out: true))->check(["uuid" => "my-uuid"]);
}
public function test_check_no_exception_if_log_out_required_and_logged_out(): void
{
(new LoginValidator(validate_logged_out: true))->check([]);
self::assertTrue(true);
}
}

View File

@ -0,0 +1,51 @@
<?php /** @noinspection PhpUnhandledExceptionInspection */
namespace com\fwdekker\deathnotifier\validation;
use PHPUnit\Framework\TestCase;
/**
* Unit tests for {@see RuleSet}.
*/
class RuleSetTest extends TestCase
{
public function test_no_exception_if_no_rule_lists(): void
{
$rules = [];
$inputs = ["key1" => "value1", "key2" => "value2"];
(new RuleSet($rules))->check($inputs);
self::assertTrue(true);
}
public function test_no_exception_if_empty_rule_lists(): void
{
$rules = ["key1" => [], "key3" => []];
$inputs = ["key1" => "value1", "key2" => "value2"];
(new RuleSet($rules))->check($inputs);
self::assertTrue(true);
}
public function test_exception_if_single_invalid_rule(): void
{
$rules = ["key1" => [new IsStringRule()]];
$inputs = ["key2" => "value2"];
self::expectException(InvalidTypeException::class);
(new RuleSet($rules))->check($inputs);
}
public function test_exception_for_first_invalid_rule_in_first_rule_list(): void
{
$rules = ["key1" => [new IsStringRule(), new IsEmailRule()], "key2" => [new IsStringRule()]];
$inputs = ["key3" => "value3"];
self::expectException(InvalidTypeException::class);
self::expectExceptionMessage("Required input 'key1' not set.");
(new RuleSet($rules))->check($inputs);
}
}

View File

@ -1,8 +1,7 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
namespace com\fwdekker\deathnotifier\validation;
use com\fwdekker\deathnotifier\IllegalArgumentError;
use PHPUnit\Framework\TestCase;
use Throwable;
@ -15,33 +14,32 @@ abstract class RuleTest extends TestCase
/**
* Tests the output of {@see Rule::check()}.
*
* @param string $name the name to give to the test case
* @param Rule $rule the rule to check
* @param mixed|null $input the user's input
* @param class-string<Throwable>|null $exception the exception that is asserted to be thrown
* @param string|null $exception_message the exception message that is asserted
* @param string|null $exception_message the exception message that is asserted to be thrown
* @return void
* @throws InvalidInputException if {@see $exception} is `null` but an exception is thrown
* @throws InvalidTypeException never
* @throws InvalidValueException never
* @dataProvider check_provider
*/
public function test_check(string $name, mixed $input, ?string $exception, ?string $exception_message): void
public function test_check(Rule $rule, mixed $input, ?string $exception, ?string $exception_message): void
{
self::setName($name);
if ($exception !== null)
self::expectException($exception);
if ($exception_message !== null)
self::expectExceptionMessage($exception_message);
(new IsEmailRule())->check(["key" => $input], "key");
$rule->check(["key" => $input], "key");
if ($exception === null)
if ($exception === null && $exception_message === null)
self::assertTrue(true);
}
/**
* Returns the test cases.
*
* @return array<array{string, mixed|null, class-string<Throwable>|null, string|null}> the test cases
* @return array<string, array{Rule, mixed|null, class-string<Throwable>|null, string|null}> the test cases
* @see RuleTest::test_check()
*/
abstract public function check_provider(): array;

View File

@ -1,31 +0,0 @@
<?php
namespace com\fwdekker\deathnotifier\validator;
use com\fwdekker\deathnotifier\IllegalArgumentError;
use PHPUnit\Framework\TestCase;
/**
* Unit tests for {@see IsEmailRule}.
*/
class IsEmailRuleTest extends RuleTest
{
public function check_provider(): array
{
$error = IllegalArgumentError::class;
$exception = InvalidInputException::class;
return [
["error if input is not set", null, $error, "Required input 'key' not set."],
["error if input is not string", 491, $error, "Input 'key' should be string, but is integer."],
["exception if input starts with space", " valid@example.com", $exception, "Remove the spaces at the start."],
["exception if input ends with space", "valid@example.com ", $exception, "Remove the spaces at the end."],
["exception if input is empty", "", $exception, "Enter an email address."],
["exception if input misses the '@' symbol", "invalid", $exception, "Don't forget to add an '@' symbol."],
["exception if input misses the domain part", "invalid@", $exception, "Add a domain name after the '@'."],
["exception if input is invalid in a complex way", "invalid@example", $exception, "Enter a valid email address."],
["no exception if input is valid", "valid@example.com", null, null],
];
}
}