Clean up server-side by, like, a lot
This commit is contained in:
parent
37db33b2f5
commit
af79597572
|
@ -119,7 +119,7 @@ dist
|
|||
|
||||
## Composer
|
||||
composer.phar
|
||||
/vendor/
|
||||
vendor/
|
||||
|
||||
# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
|
||||
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
|
||||
|
|
|
@ -8,7 +8,7 @@ module.exports = grunt => {
|
|||
},
|
||||
copy: {
|
||||
composer: {
|
||||
files: [{expand: true, cwd: "./", src: "vendor/**", dest: "dist/", flatten: false}]
|
||||
files: [{expand: true, cwd: "./vendor/", src: "**", dest: "dist/.vendor/", flatten: false}]
|
||||
},
|
||||
css: {
|
||||
files: [{expand: true, cwd: "src/main/", src: "**/*.css", dest: "dist/", flatten: true}]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "fwdekker/death-notifier",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"version": "0.0.23",
|
||||
"version": "0.0.24",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"homepage": "https://git.fwdekker.com/tools/death-notifier",
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "death-notifier",
|
||||
"version": "0.0.23",
|
||||
"version": "0.0.24",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"author": "Florine W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
265
src/main/api.php
265
src/main/api.php
|
@ -1,191 +1,137 @@
|
|||
<?php
|
||||
|
||||
use Monolog\ErrorHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
use php\Database;
|
||||
use php\EmailRule;
|
||||
use php\EqualsRule;
|
||||
use php\IsNotBlankRule;
|
||||
use php\IsNotSetRule;
|
||||
use php\IsSetRule;
|
||||
use php\LengthRule;
|
||||
use php\Mailer;
|
||||
use php\Mediawiki;
|
||||
use php\Response;
|
||||
use php\TrackingManager;
|
||||
use php\UserManager;
|
||||
use php\Util;
|
||||
use php\Validator;
|
||||
|
||||
/** @noinspection PhpIncludeInspection Exists after `npm run deploy` */
|
||||
require_once __DIR__ . "/vendor/autoload.php";
|
||||
require_once __DIR__ . "/.vendor/autoload.php";
|
||||
|
||||
|
||||
/// Preparations
|
||||
// Load config
|
||||
// TODO: Check permissions, exit if too permissive
|
||||
$config = parse_ini_file("config.default.ini.php", process_sections: true, scanner_mode: INI_SCANNER_TYPED) or exit(1);
|
||||
if (file_exists("config.ini.php")) {
|
||||
$config_custom =
|
||||
parse_ini_file("config.ini.php", process_sections: true, scanner_mode: INI_SCANNER_TYPED) or exit(1);
|
||||
$config = array_merge($config, $config_custom);
|
||||
}
|
||||
|
||||
// Create logger
|
||||
$logger = new Logger("main");
|
||||
$logger->pushHandler(new StreamHandler($config["logger"]["file"], $config["logger"]["level"]));
|
||||
ErrorHandler::register($logger);
|
||||
|
||||
// Connect to database
|
||||
$db_exists = file_exists($config["database"]["filename"]);
|
||||
// Preamble
|
||||
$_POST = Util::parse_post();
|
||||
$config = Util::read_config() ?? Util::http_exit(500);
|
||||
$logger = Util::create_logger($config["logger"]);
|
||||
$conn = Database::connect($config["database"]["filename"]);
|
||||
|
||||
// Instantiate utility classes
|
||||
$mediawiki = new Mediawiki($logger->withName("Mediawiki"));
|
||||
$user_manager = new UserManager($conn);
|
||||
$tracking_manager = new TrackingManager($mediawiki, $conn);
|
||||
$mailer = new Mailer($logger->withName("Mailer"), $config, $conn);
|
||||
$mediawiki = new Mediawiki($logger->withName("Mediawiki"));
|
||||
$user_manager = new UserManager($conn, $mailer);
|
||||
$tracking_manager = new TrackingManager($conn, $mailer, $mediawiki);
|
||||
|
||||
// Create db if it does not exist
|
||||
if (!$db_exists) {
|
||||
$logger->warning("Database does not exist. Creating new database at '{$config["database"]["filename"]}'.");
|
||||
|
||||
if (Database::is_empty($conn)) {
|
||||
$logger->info("Database does not exist. Creating new database at '{$config["database"]["filename"]}'.");
|
||||
$mailer->install();
|
||||
$user_manager->install();
|
||||
$tracking_manager->install();
|
||||
$mailer->install();
|
||||
}
|
||||
|
||||
// Start session
|
||||
/**
|
||||
* Generates a CSRF token.
|
||||
*
|
||||
* @return string the generated CSRF token
|
||||
*/
|
||||
function generate_csrf_token(Logger $logger): string
|
||||
{
|
||||
try {
|
||||
return bin2hex(random_bytes(32));
|
||||
} catch (Exception $exception) {
|
||||
$logger->emergency("Failed to generate token.", ["cause" => $exception]);
|
||||
http_response_code(500);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
session_start();
|
||||
if (!isset($_SESSION["token"]))
|
||||
$_SESSION["token"] = generate_csrf_token($logger);
|
||||
|
||||
// Read JSON from POST
|
||||
$post_input = file_get_contents("php://input");
|
||||
if ($post_input !== false) $_POST = json_decode($post_input, associative: true);
|
||||
$_SESSION["token"] = $_SESSION["token"] ?? Util::generate_csrf_token($logger);
|
||||
|
||||
|
||||
/// Define validation helpers
|
||||
/**
|
||||
* Validates the user's CSRF token by comparing `$_POST["token"]` against `$_SESSION["token"]`.
|
||||
*
|
||||
* @return Response|null `null` if the user's CSRF token is valid, or an error response if either value is not set or if
|
||||
* the two are not equal
|
||||
*/
|
||||
function validate_csrf(): ?Response
|
||||
{
|
||||
if (!(isset($_POST["token"]) && isset($_SESSION["token"]) && hash_equals($_POST["token"], $_SESSION["token"])))
|
||||
return new Response(
|
||||
payload: ["target" => null, "message" => "Invalid CSRF token. Try refreshing the page."],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the user is logged out by checking that `$_SESSION["uuid"]` is not set.
|
||||
*
|
||||
* @return Response|null `null` if the user is logged out, or an error response otherwise
|
||||
*/
|
||||
function validate_logged_out(): ?Response
|
||||
{
|
||||
if (isset($_SESSION["uuid"]))
|
||||
return new Response(payload: ["target" => null, "message" => "User is already logged in."], satisfied: false);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the user is logged in by checking that `$_SESSION["uuid"]` is set.
|
||||
*
|
||||
* @return Response|null `null` if the user is logged in, or an error response otherwise
|
||||
*/
|
||||
function validate_logged_in(): ?Response
|
||||
{
|
||||
if (!isset($_SESSION["uuid"]))
|
||||
return new Response(payload: ["target" => null, "message" => "User is not logged in."], satisfied: false);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that each of `arguments` is set.
|
||||
*
|
||||
* @param mixed ...$arguments the arguments to check
|
||||
* @return Response|null `null` if all `arguments` are set, or an error response otherwise
|
||||
*/
|
||||
function validate_has_arguments(mixed ...$arguments): ?Response
|
||||
{
|
||||
foreach (func_get_args() as $argument) {
|
||||
if (!isset($argument))
|
||||
return new Response(payload: ["target" => null, "message" => "Missing argument."], satisfied: false);
|
||||
if (!is_string($argument))
|
||||
return new Response(payload: ["target" => null, "message" => "Invalid argument type."], satisfied: false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// Process request
|
||||
// Process request
|
||||
$response = null;
|
||||
|
||||
if (isset($_POST["action"])) {
|
||||
// POST requests; alter state
|
||||
switch ($_POST["action"]) {
|
||||
case "register":
|
||||
$response = validate_csrf()
|
||||
?? validate_logged_out()
|
||||
?? validate_has_arguments($_POST["email"], $_POST["password"], $_POST["password_confirm"])
|
||||
?? $user_manager->register($mailer, $_POST["email"], $_POST["password"], $_POST["password_confirm"]);
|
||||
$response =
|
||||
Validator::validate_inputs($_SESSION, ["uuid" => [new IsNotSetRule()]])
|
||||
?? Validator::validate_inputs($_POST,
|
||||
[
|
||||
"token" => [new EqualsRule($_SESSION["token"])],
|
||||
"email" => [new IsSetRule(), new EmailRule()],
|
||||
"password" => [
|
||||
new IsSetRule(),
|
||||
new LengthRule(UserManager::MIN_PASSWORD_LENGTH, UserManager::MAX_PASSWORD_LENGTH)
|
||||
],
|
||||
"password_confirm" => [new IsSetRule()],
|
||||
])
|
||||
?? $user_manager->register($_POST["email"], $_POST["password"], $_POST["password_confirm"]);
|
||||
break;
|
||||
case "login":
|
||||
$response = validate_csrf()
|
||||
?? validate_logged_out()
|
||||
?? validate_has_arguments($_POST["email"], $_POST["password"]);
|
||||
$response =
|
||||
Validator::validate_inputs($_SESSION, ["uuid" => [new IsNotSetRule()]])
|
||||
?? Validator::validate_inputs($_POST,
|
||||
[
|
||||
"token" => [new EqualsRule($_SESSION["token"])],
|
||||
"email" => [new IsSetRule(), new EmailRule()],
|
||||
"password" => [
|
||||
new IsSetRule(),
|
||||
new LengthRule(UserManager::MIN_PASSWORD_LENGTH, UserManager::MAX_PASSWORD_LENGTH)
|
||||
],
|
||||
]);
|
||||
if ($response !== null) break;
|
||||
|
||||
[$response, $uuid] = $user_manager->check_login($_POST["email"], $_POST["password"]);
|
||||
if ($response->satisfied) $_SESSION["uuid"] = $uuid;
|
||||
break;
|
||||
case "logout":
|
||||
$response = validate_csrf() ?? validate_logged_in();
|
||||
$response =
|
||||
Validator::validate_inputs($_SESSION, ["uuid" => [new IsSetRule()]])
|
||||
?? Validator::validate_inputs($_POST, ["token" => [new EqualsRule($_SESSION["token"])]]);
|
||||
if ($response !== null) break;
|
||||
|
||||
session_destroy();
|
||||
session_start();
|
||||
$_SESSION["token"] = generate_csrf_token($logger);
|
||||
$_SESSION["token"] = Util::generate_csrf_token($logger) ?? Util::http_exit(500);
|
||||
$response = new Response(payload: null, satisfied: true);
|
||||
break;
|
||||
case "update-email":
|
||||
$response = validate_csrf()
|
||||
?? validate_logged_in()
|
||||
?? validate_has_arguments($_POST["email"])
|
||||
?? $user_manager->set_email($_SESSION["uuid"], $_POST["email"], $mailer);
|
||||
$response =
|
||||
Validator::validate_inputs($_SESSION, ["uuid" => [new IsSetRule()]])
|
||||
?? Validator::validate_inputs($_POST,
|
||||
[
|
||||
"token" => [new EqualsRule($_SESSION["token"])],
|
||||
"email" => [new IsSetRule(), new EmailRule()],
|
||||
])
|
||||
?? $user_manager->set_email($_SESSION["uuid"], $_POST["email"]);
|
||||
break;
|
||||
case "verify-email":
|
||||
$response = validate_has_arguments($_POST["email"], $_POST["token"])
|
||||
$response =
|
||||
Validator::validate_inputs($_SESSION, ["uuid" => [new IsSetRule()]])
|
||||
?? Validator::validate_inputs($_POST,
|
||||
[
|
||||
"token" => [new IsSetRule()],
|
||||
"email" => [new IsSetRule(), new EmailRule()],
|
||||
])
|
||||
?? $user_manager->verify_email($_POST["email"], $_POST["token"]);
|
||||
break;
|
||||
case "resend-verify-email":
|
||||
$response = validate_csrf()
|
||||
?? validate_logged_in()
|
||||
?? $user_manager->resend_verify_email($_SESSION["uuid"], $mailer);
|
||||
$response =
|
||||
Validator::validate_inputs($_SESSION, ["uuid" => [new IsSetRule()]])
|
||||
?? Validator::validate_inputs($_POST, ["token" => [new EqualsRule($_SESSION["token"])]])
|
||||
?? $user_manager->resend_verify_email($_SESSION["uuid"]);
|
||||
break;
|
||||
case "update-password":
|
||||
$response = validate_csrf()
|
||||
?? validate_logged_in()
|
||||
?? validate_has_arguments($_POST["password_old"], $_POST["password_new"], $_POST["password_confirm"])
|
||||
$response =
|
||||
Validator::validate_inputs($_SESSION, ["uuid" => [new IsSetRule()]])
|
||||
?? Validator::validate_inputs($_POST,
|
||||
[
|
||||
"token" => [new EqualsRule($_SESSION["token"])],
|
||||
"password_old" => [
|
||||
new IsSetRule(),
|
||||
new LengthRule(UserManager::MIN_PASSWORD_LENGTH, UserManager::MAX_PASSWORD_LENGTH)
|
||||
],
|
||||
"password_new" => [
|
||||
new IsSetRule(),
|
||||
new LengthRule(UserManager::MIN_PASSWORD_LENGTH, UserManager::MAX_PASSWORD_LENGTH)
|
||||
],
|
||||
"password_confirm" => [new IsSetRule()],
|
||||
])
|
||||
?? $user_manager->set_password(
|
||||
$_SESSION["uuid"],
|
||||
$_POST["password_old"],
|
||||
|
@ -194,20 +140,37 @@ if (isset($_POST["action"])) {
|
|||
);
|
||||
break;
|
||||
case "user-delete":
|
||||
$response = validate_csrf()
|
||||
?? validate_logged_in()
|
||||
$response =
|
||||
Validator::validate_inputs($_SESSION, ["uuid" => [new IsSetRule()]])
|
||||
?? Validator::validate_inputs($_POST, ["token" => [new EqualsRule($_SESSION["token"])]])
|
||||
?? $user_manager->delete($_SESSION["uuid"]);
|
||||
break;
|
||||
case "add-tracking":
|
||||
$response = validate_csrf()
|
||||
?? validate_logged_in()
|
||||
?? validate_has_arguments($_POST["person_name"])
|
||||
$response =
|
||||
Validator::validate_inputs($_SESSION, ["uuid" => [new IsSetRule()]])
|
||||
?? Validator::validate_inputs($_POST,
|
||||
[
|
||||
"token" => [new EqualsRule($_SESSION["token"])],
|
||||
"person_name" => [
|
||||
new IsSetRule(),
|
||||
new LengthRule(TrackingManager::MIN_TITLE_LENGTH, TrackingManager::MAX_TITLE_LENGTH),
|
||||
new IsNotBlankRule()
|
||||
],
|
||||
])
|
||||
?? $tracking_manager->add_tracking($_SESSION["uuid"], $_POST["person_name"]);
|
||||
break;
|
||||
case "remove-tracking":
|
||||
$response = validate_csrf()
|
||||
?? validate_logged_in()
|
||||
?? validate_has_arguments($_POST["person_name"])
|
||||
$response =
|
||||
Validator::validate_inputs($_SESSION, ["uuid" => [new IsSetRule()]])
|
||||
?? Validator::validate_inputs($_POST,
|
||||
[
|
||||
"token" => [new EqualsRule($_SESSION["token"])],
|
||||
"person_name" => [
|
||||
new IsSetRule(),
|
||||
new LengthRule(TrackingManager::MIN_TITLE_LENGTH, TrackingManager::MAX_TITLE_LENGTH),
|
||||
new IsNotBlankRule()
|
||||
],
|
||||
])
|
||||
?? $tracking_manager->remove_tracking($_SESSION["uuid"], $_POST["person_name"]);
|
||||
break;
|
||||
default:
|
||||
|
@ -230,14 +193,18 @@ if (isset($_POST["action"])) {
|
|||
if (!$response->satisfied) {
|
||||
session_destroy();
|
||||
session_start();
|
||||
$_SESSION["token"] = generate_csrf_token($logger);
|
||||
$_SESSION["token"] = Util::generate_csrf_token($logger) ?? Util::http_exit(500);
|
||||
}
|
||||
break;
|
||||
case "get-user-data":
|
||||
$response = validate_logged_in() ?? $user_manager->get_user_data($_SESSION["uuid"]);
|
||||
$response =
|
||||
Validator::validate_inputs($_SESSION, ["uuid" => [new IsSetRule()]])
|
||||
?? $user_manager->get_user_data($_SESSION["uuid"]);
|
||||
break;
|
||||
case "list-trackings":
|
||||
$response = validate_logged_in() ?? $tracking_manager->list_trackings($_SESSION["uuid"]);
|
||||
$response =
|
||||
Validator::validate_inputs($_SESSION, ["uuid" => [new IsSetRule()]])
|
||||
?? $tracking_manager->list_trackings($_SESSION["uuid"]);
|
||||
break;
|
||||
default:
|
||||
$response = new Response(
|
||||
|
@ -255,7 +222,7 @@ if (isset($_POST["action"])) {
|
|||
switch ($argv[1]) {
|
||||
case "update-all-trackings":
|
||||
$logger->info("Updating all trackings.");
|
||||
$tracking_manager->update_trackings($mailer, $tracking_manager->list_all_unique_person_names());
|
||||
$tracking_manager->update_trackings($tracking_manager->list_all_unique_person_names());
|
||||
exit("Successfully updated all trackings.");
|
||||
case "process-email-queue":
|
||||
$logger->info("Processing email queue.");
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
</label>
|
||||
<label for="registerPasswordConfirm">
|
||||
Confirm password
|
||||
<input id="registerPasswordConfirm" type="password" name="passwordConfirm" />
|
||||
<input id="registerPasswordConfirm" type="password" name="password_confirm" />
|
||||
<span class="validationInfo"></span>
|
||||
</label>
|
||||
<!-- TODO: Display loading indicator while waiting for form to complete -->
|
||||
|
@ -119,7 +119,7 @@
|
|||
<span class="validationInfo"></span>
|
||||
</p>
|
||||
<label for="addTrackingPersonName">
|
||||
<input id="addTrackingPersonName" type="text" name="personName"
|
||||
<input id="addTrackingPersonName" type="text" name="person_name"
|
||||
autocomplete="on" />
|
||||
<button id="addTrackingButton">Add</button>
|
||||
<span class="validationInfo"></span>
|
||||
|
@ -179,17 +179,17 @@
|
|||
</p>
|
||||
<label for="updatePasswordPasswordOld">
|
||||
Old password
|
||||
<input id="updatePasswordPasswordOld" type="password" name="passwordOld" />
|
||||
<input id="updatePasswordPasswordOld" type="password" name="password_old" />
|
||||
<span class="validationInfo"></span>
|
||||
</label>
|
||||
<label for="updatePasswordPasswordNew">
|
||||
New password
|
||||
<input id="updatePasswordPasswordNew" type="password" name="passwordNew" />
|
||||
<input id="updatePasswordPasswordNew" type="password" name="password_new" />
|
||||
<span class="validationInfo"></span>
|
||||
</label>
|
||||
<label for="updatePasswordPasswordConfirm">
|
||||
Confirm new password
|
||||
<input id="updatePasswordPasswordConfirm" type="password" name="passwordConfirm" />
|
||||
<input id="updatePasswordPasswordConfirm" type="password" name="password_confirm" />
|
||||
<span class="validationInfo"></span>
|
||||
</label>
|
||||
<button id="updatePasswordButton">Change password</button>
|
||||
|
|
|
@ -24,5 +24,19 @@ class Database
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns `true` if no tables exist in the database.
|
||||
*
|
||||
* @param PDO $conn the database connection to use to check
|
||||
* @return bool `true` if no tables exist in the database
|
||||
*/
|
||||
public static function is_empty(PDO $conn): bool
|
||||
{
|
||||
$stmt = $conn->prepare("SELECT count(*) FROM sqlite_master WHERE type = 'table';");
|
||||
$stmt->execute();
|
||||
return $stmt->fetch()[0] === 0;
|
||||
}
|
||||
|
||||
|
||||
// TODO: Add version number, etc., and auto-migration
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ class Mailer
|
|||
PRIMARY KEY (type, arg1, arg2));");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Queues an email to be sent for a newly registered user.
|
||||
*
|
||||
|
@ -174,6 +175,7 @@ class Mailer
|
|||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sends all emails in the queue.
|
||||
*
|
||||
|
|
|
@ -26,7 +26,7 @@ class Mediawiki
|
|||
*
|
||||
* Since the record for a single page is 252 categories, setting to the maximum of 500 is more than sufficient.
|
||||
*/
|
||||
private const CATS_PER_QUERY = "500";
|
||||
private const CATS_PER_QUERY = 500;
|
||||
|
||||
/**
|
||||
* @var Logger The logger to use for logging.
|
||||
|
@ -171,7 +171,7 @@ class Mediawiki
|
|||
*/
|
||||
public function people_statuses(array $people_names): QueryOutput
|
||||
{
|
||||
$output = $this->api_query(["prop" => "categories", "cllimit" => self::CATS_PER_QUERY], $people_names);
|
||||
$output = $this->api_query(["prop" => "categories", "cllimit" => strval(self::CATS_PER_QUERY)], $people_names);
|
||||
|
||||
$pages =
|
||||
array_combine(
|
||||
|
|
|
@ -21,6 +21,9 @@ class Response
|
|||
/**
|
||||
* Constructs a new response.
|
||||
*
|
||||
* If `satisfied` is `false`, then `payload` must be an array containing at least the strings `target` and
|
||||
* `message`. Otherwise, `payload` may be anything.
|
||||
*
|
||||
* @param mixed $payload the payload corresponding to the client's query
|
||||
* @param bool $satisfied `true` if and only if the request was fully completed
|
||||
*/
|
||||
|
|
|
@ -10,30 +10,41 @@ use PDO;
|
|||
*/
|
||||
class TrackingManager
|
||||
{
|
||||
/**
|
||||
* The minimum length of a Wikipedia page title.
|
||||
*/
|
||||
public const MIN_TITLE_LENGTH = 1;
|
||||
/**
|
||||
* The maximum length of a Wikipedia page title.
|
||||
*/
|
||||
private const MAX_TITLE_LENGTH = 255;
|
||||
public const MAX_TITLE_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* @var Mediawiki The Mediawiki instance to use for interacting with Wikipedia.
|
||||
*/
|
||||
private Mediawiki $mediawiki;
|
||||
/**
|
||||
* @var PDO The database connection to interact with.
|
||||
*/
|
||||
private PDO $conn;
|
||||
/**
|
||||
* @var Mailer The mailer to send emails with.
|
||||
*/
|
||||
private Mailer $mailer;
|
||||
/**
|
||||
* @var Mediawiki The Mediawiki instance to use for interacting with Wikipedia.
|
||||
*/
|
||||
private Mediawiki $mediawiki;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new tracking manager.
|
||||
*
|
||||
* @param PDO $conn the database connection to interact with
|
||||
* @param Mailer $mailer the mailer to send emails with
|
||||
* @param Mediawiki $mediawiki the Mediawiki instance to use for interacting with Wikipedia
|
||||
*/
|
||||
public function __construct(Mediawiki $mediawiki, PDO $conn)
|
||||
public function __construct(PDO $conn, Mailer $mailer, Mediawiki $mediawiki)
|
||||
{
|
||||
$this->mediawiki = $mediawiki;
|
||||
$this->conn = $conn;
|
||||
$this->mailer = $mailer;
|
||||
$this->mediawiki = $mediawiki;
|
||||
}
|
||||
|
||||
|
||||
|
@ -44,8 +55,8 @@ class TrackingManager
|
|||
*/
|
||||
public function install(): void
|
||||
{
|
||||
$this->conn->exec("CREATE TABLE trackings(user_uuid text not null,
|
||||
person_name text not null,
|
||||
$this->conn->exec("CREATE TABLE trackings(user_uuid TEXT NOT NULL,
|
||||
person_name TEXT NOT NULL,
|
||||
PRIMARY KEY (user_uuid, person_name),
|
||||
FOREIGN KEY (user_uuid) REFERENCES users (uuid)
|
||||
ON DELETE CASCADE
|
||||
|
@ -53,9 +64,9 @@ class TrackingManager
|
|||
FOREIGN KEY (person_name) REFERENCES people (name)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE);");
|
||||
$this->conn->exec("CREATE TABLE people(name text not null unique primary key,
|
||||
status text not null,
|
||||
is_deleted int not null default 0);");
|
||||
$this->conn->exec("CREATE TABLE people(name TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||
status TEXT NOT NULL DEFAULT '',
|
||||
is_deleted INT NOT NULL DEFAULT 0);");
|
||||
$this->conn->exec("CREATE TRIGGER people_cull_orphans
|
||||
AFTER DELETE ON trackings
|
||||
FOR EACH ROW
|
||||
|
@ -65,26 +76,17 @@ class TrackingManager
|
|||
END;");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a tracking to the database.
|
||||
*
|
||||
* @param string $user_uuid the user to whom the tracking belongs
|
||||
* @param string $person_name the name of the person to track
|
||||
* @return Response a response with no message, and a status indicating whether the insertion succeeded
|
||||
* @return Response a satisfied `Response` with payload `null` if the tracking was added, or an unsatisfied
|
||||
* `Response` otherwise
|
||||
*/
|
||||
public function add_tracking(string $user_uuid, string $person_name): Response
|
||||
{
|
||||
if (trim($person_name) === "")
|
||||
return new Response(
|
||||
payload: ["target" => "personName", "message" => "Invalid page name: empty."],
|
||||
satisfied: false
|
||||
);
|
||||
if (strlen($person_name) > self::MAX_TITLE_LENGTH)
|
||||
return new Response(
|
||||
payload: ["target" => "personName", "message" => "Invalid page name: too long."],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
$pages_exist = $this->mediawiki->pages_exist([$person_name]);
|
||||
$normalized_name = $pages_exist->redirects[$person_name];
|
||||
if (in_array($normalized_name, $pages_exist->missing))
|
||||
|
@ -104,15 +106,13 @@ class TrackingManager
|
|||
// Insert person and tracking
|
||||
$this->conn->beginTransaction();
|
||||
|
||||
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO people (name, status)
|
||||
VALUES (:name, :status);");
|
||||
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO people (name) VALUES (:name);");
|
||||
$stmt->bindValue(":name", $normalized_name);
|
||||
$stmt->bindValue(":status", $status->value);
|
||||
$stmt->execute();
|
||||
|
||||
$stmt = $this->conn->prepare("UPDATE people SET status=:status WHERE name=:name;");
|
||||
$stmt->bindValue(":status", $status->value);
|
||||
$stmt->bindValue(":name", $normalized_name);
|
||||
$stmt->bindValue(":status", $status->value);
|
||||
$stmt->execute();
|
||||
|
||||
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO trackings (user_uuid, person_name)
|
||||
|
@ -130,16 +130,10 @@ class TrackingManager
|
|||
*
|
||||
* @param string $user_uuid the user to whom the tracking belongs
|
||||
* @param string $person_name the name of the tracked person to remove
|
||||
* @return Response a response with no message, and a status indicating whether the removal succeeded
|
||||
* @return Response a satisfied `Response` with payload `null`
|
||||
*/
|
||||
public function remove_tracking(string $user_uuid, string $person_name): Response
|
||||
{
|
||||
if (strlen($person_name) > self::MAX_TITLE_LENGTH)
|
||||
return new Response(
|
||||
payload: ["target" => "personName", "message" => "Invalid page name: too long."],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
$stmt = $this->conn->prepare("DELETE FROM trackings
|
||||
WHERE user_uuid=:user_uuid AND person_name=:person_name;");
|
||||
$stmt->bindValue(":user_uuid", $user_uuid);
|
||||
|
@ -153,7 +147,7 @@ class TrackingManager
|
|||
* Lists all trackings of the indicated user.
|
||||
*
|
||||
* @param string $user_uuid the user to return the trackings of
|
||||
* @return Response a response with all trackings of the indicated user
|
||||
* @return Response a satisfied `Response` with the trackings as its payload
|
||||
*/
|
||||
public function list_trackings(string $user_uuid): Response
|
||||
{
|
||||
|
@ -169,10 +163,11 @@ class TrackingManager
|
|||
return new Response(payload: $results, satisfied: true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Lists all unique names being tracked in the database.
|
||||
*
|
||||
* @return array<string> the unique records in the database
|
||||
* @return array<string> all unique names in the database
|
||||
*/
|
||||
public function list_all_unique_person_names(): array
|
||||
{
|
||||
|
@ -184,23 +179,21 @@ class TrackingManager
|
|||
/**
|
||||
* Updates trackings for the given names.
|
||||
*
|
||||
* @param Mailer $mailer the mailer to notify the user with
|
||||
* @param string[] $people_names the names of the pages to update the tracking of
|
||||
* @return void
|
||||
*/
|
||||
public function update_trackings(Mailer $mailer, array $people_names): void
|
||||
public function update_trackings(array $people_names): void
|
||||
{
|
||||
if (empty($people_names)) return;
|
||||
|
||||
// Fetch statuses from Mediawiki
|
||||
$people_statuses = $this->mediawiki->people_statuses($people_names);
|
||||
|
||||
// Begin transaction
|
||||
$this->conn->beginTransaction();
|
||||
|
||||
// Process redirects
|
||||
$stmt = $this->conn->prepare("UPDATE people
|
||||
SET name=:new_name
|
||||
WHERE name=:old_name;");
|
||||
$stmt = $this->conn->prepare("UPDATE people SET name=:new_name WHERE name=:old_name;");
|
||||
$stmt->bindParam(":old_name", $from);
|
||||
$stmt->bindParam(":new_name", $to);
|
||||
foreach ($people_statuses->redirects as $from => $to)
|
||||
|
@ -208,9 +201,7 @@ class TrackingManager
|
|||
$stmt->execute();
|
||||
|
||||
// Process deletions
|
||||
$stmt = $this->conn->prepare("UPDATE people
|
||||
SET is_deleted=1
|
||||
WHERE name=:name;");
|
||||
$stmt = $this->conn->prepare("UPDATE people SET is_deleted=1 WHERE name=:name;");
|
||||
$stmt->bindParam(":name", $missing_title);
|
||||
foreach ($people_statuses->missing as $missing_title)
|
||||
// TODO: Inform user that page has been deleted
|
||||
|
@ -242,7 +233,7 @@ class TrackingManager
|
|||
if (sizeof($stmt_status->fetchAll(PDO::FETCH_ASSOC)) > 0 && $person_status === PersonStatus::Alive) {
|
||||
$stmt_emails->execute();
|
||||
foreach (array_column($stmt_emails->fetchAll(PDO::FETCH_ASSOC), "email") as $user_email)
|
||||
$mailer->queue_death_notification($user_email, $person_name);
|
||||
$this->mailer->queue_death_notification($user_email, $person_name);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,18 +12,14 @@ use PDO;
|
|||
// TODO: Remove duplication in this class
|
||||
class UserManager
|
||||
{
|
||||
/**
|
||||
* The maximum length of an email address.
|
||||
*/
|
||||
private const MAX_EMAIL_LENGTH = 254;
|
||||
/**
|
||||
* The minimum length of a password;
|
||||
*/
|
||||
private const MIN_PASSWORD_LENGTH = 8;
|
||||
public const MIN_PASSWORD_LENGTH = 8;
|
||||
/**
|
||||
* The maximum length of a password.
|
||||
*/
|
||||
private const MAX_PASSWORD_LENGTH = 64;
|
||||
public const MAX_PASSWORD_LENGTH = 64;
|
||||
/**
|
||||
* The minimum number of minutes between two verification emails.
|
||||
*/
|
||||
|
@ -33,16 +29,22 @@ class UserManager
|
|||
* @var PDO The database connection to interact with.
|
||||
*/
|
||||
private PDO $conn;
|
||||
/**
|
||||
* @var Mailer The mailer to send emails with.
|
||||
*/
|
||||
private Mailer $mailer;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new user manager.
|
||||
*
|
||||
* @param PDO $conn the database connection to interact with
|
||||
* @param Mailer $mailer the mailer to send emails with
|
||||
*/
|
||||
public function __construct(PDO $conn)
|
||||
public function __construct(PDO $conn, Mailer $mailer)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
$this->mailer = $mailer;
|
||||
}
|
||||
|
||||
|
||||
|
@ -53,43 +55,29 @@ class UserManager
|
|||
*/
|
||||
public function install(): void
|
||||
{
|
||||
$this->conn->exec("CREATE TABLE users(uuid text not null unique primary key default(lower(hex(randomblob(16)))),
|
||||
email text not null unique,
|
||||
email_verification_token text,
|
||||
email_verification_token_timestamp int not null,
|
||||
password text not null,
|
||||
password_update_time int not null);");
|
||||
$this->conn->exec("CREATE TABLE users(uuid TEXT NOT NULL UNIQUE PRIMARY KEY DEFAULT(lower(hex(randomblob(16)))),
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
email_verification_token TEXT,
|
||||
email_verification_token_timestamp INT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
password_update_time INT NOT NULL);");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Registers a new user.
|
||||
*
|
||||
* @param Mailer $mailer the mailer to notify the user with
|
||||
* @param string $email the user-submitted email address
|
||||
* @param string $password the user-submitted password
|
||||
* @param string $password_confirm the user-submitted password confirmation
|
||||
* @return Response a response with message `null` if the user was registered, or a response with a message
|
||||
* explaining what went wrong otherwise
|
||||
* @return Response a satisfied `Response` with payload `null` if the registration was successful, or an unsatisfied
|
||||
* `Response` otherwise
|
||||
*/
|
||||
public function register(Mailer $mailer, string $email, string $password, string $password_confirm): Response
|
||||
public function register(string $email, string $password, string $password_confirm): Response
|
||||
{
|
||||
// Validate
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > self::MAX_EMAIL_LENGTH)
|
||||
return new Response(
|
||||
payload: ["target" => "email", "message" => "Invalid email address."],
|
||||
satisfied: false
|
||||
);
|
||||
if (strlen($password) < self::MIN_PASSWORD_LENGTH || strlen($password) > self::MAX_PASSWORD_LENGTH)
|
||||
return new Response(
|
||||
payload: [
|
||||
"target" => "password",
|
||||
"message" => "Your password should be at least 8 and at most 64 characters long."
|
||||
],
|
||||
satisfied: false
|
||||
);
|
||||
if ($password !== $password_confirm)
|
||||
return new Response(
|
||||
payload: ["target" => "passwordConfirm", "message" => "Passwords do not match."],
|
||||
payload: ["target" => "password_confirm", "message" => "Passwords do not match."],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
|
@ -97,13 +85,11 @@ class UserManager
|
|||
$this->conn->beginTransaction();
|
||||
|
||||
// Check if email address is already in use
|
||||
$stmt = $this->conn->prepare("SELECT COUNT(*) AS count
|
||||
FROM users
|
||||
WHERE email=:email;");
|
||||
$stmt = $this->conn->prepare("SELECT EXISTS(SELECT 1 FROM users WHERE email=:email);");
|
||||
$stmt->bindValue(":email", $email);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($result["count"] > 0) {
|
||||
$result = $stmt->fetch();
|
||||
if ($result[0] === 1) {
|
||||
$this->conn->rollBack();
|
||||
return new Response(
|
||||
payload: ["target" => "email", "message" => "Email address already in use."],
|
||||
|
@ -124,13 +110,14 @@ class UserManager
|
|||
unixepoch())
|
||||
RETURNING email_verification_token;");
|
||||
$stmt->bindValue(":email", $email);
|
||||
// TODO: Specify password hash function, for forwards compatibility
|
||||
$stmt->bindValue(":password", password_hash($password, PASSWORD_DEFAULT));
|
||||
$stmt->execute();
|
||||
$email_verification_token = $stmt->fetchAll(PDO::FETCH_ASSOC)[0]["email_verification_token"];
|
||||
$this->mailer->queue_registration($email, $email_verification_token);
|
||||
|
||||
// Respond
|
||||
$this->conn->commit();
|
||||
$mailer->queue_registration($email, $email_verification_token);
|
||||
return new Response(payload: null, satisfied: true);
|
||||
}
|
||||
|
||||
|
@ -138,19 +125,13 @@ class UserManager
|
|||
* Deletes the user with the given UUID.
|
||||
*
|
||||
* @param string $uuid the UUID of the user to delete
|
||||
* @return Response a response with message `null` if the user was deleted, or a response with a message explaining
|
||||
* what went wrong otherwise
|
||||
* @return Response a satisfied `Response` with payload `null`
|
||||
*/
|
||||
public function delete(string $uuid): Response
|
||||
{
|
||||
$stmt = $this->conn->prepare("DELETE FROM users WHERE uuid=:uuid;");
|
||||
$stmt->bindValue(":uuid", $uuid);
|
||||
$stmt->execute();
|
||||
|
||||
$stmt = $this->conn->prepare("DELETE FROM trackings WHERE user_uuid=:uuid");
|
||||
$stmt->bindValue(":uuid", $uuid);
|
||||
$stmt->execute();
|
||||
|
||||
return new Response(payload: null, satisfied: true);
|
||||
}
|
||||
|
||||
|
@ -159,27 +140,11 @@ class UserManager
|
|||
*
|
||||
* @param string $email the email address of the user whose password should be checked
|
||||
* @param string $password the password to check against the specified user
|
||||
* @return array{0: Response, 1: string|null} a response and the user's UUID
|
||||
* @return array{Response, string|null} a satisfied `Response` with payload `null` and the user's UUID if the
|
||||
* password is correct, or an unsatisfied `Response` and `null` otherwise
|
||||
*/
|
||||
public function check_login(string $email, string $password): array
|
||||
{
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > self::MAX_EMAIL_LENGTH)
|
||||
return [
|
||||
new Response(
|
||||
payload: ["target" => "email", "message" => "Invalid email address."],
|
||||
satisfied: false
|
||||
),
|
||||
null
|
||||
];
|
||||
if (strlen($password) > self::MAX_PASSWORD_LENGTH)
|
||||
return [
|
||||
new Response(
|
||||
payload: ["target" => "password", "message" => "Incorrect combination of email and password."],
|
||||
satisfied: false
|
||||
),
|
||||
null
|
||||
];
|
||||
|
||||
$stmt = $this->conn->prepare("SELECT uuid, password FROM users WHERE email=:email;");
|
||||
$stmt->bindValue(":email", $email);
|
||||
$stmt->execute();
|
||||
|
@ -202,7 +167,8 @@ class UserManager
|
|||
* Returns a satisfied response if a user with the given UUID exists, or an unsatisfied response otherwise.
|
||||
*
|
||||
* @param string $uuid the UUID of the user to check
|
||||
* @return Response a satisfied response if a user with the given UUID exists, or an unsatisfied response otherwise
|
||||
* @return Response a satisfied `Response` with payload `null` if a user with the given UUID exists, or an
|
||||
* unsatisfied `Response` otherwise
|
||||
*/
|
||||
public function user_exists(string $uuid): Response
|
||||
{
|
||||
|
@ -220,7 +186,8 @@ class UserManager
|
|||
* Returns the user with the given UUID.
|
||||
*
|
||||
* @param string $uuid the UUID of the user to return
|
||||
* @return Response the user with the given UUID, or a response with an explanation what went wrong otherwise
|
||||
* @return Response a satisfied `Response` with the user's data if the user exists, or an unsatisfied `Response`
|
||||
* otherwise
|
||||
*/
|
||||
public function get_user_data(string $uuid): Response
|
||||
{
|
||||
|
@ -245,19 +212,11 @@ class UserManager
|
|||
*
|
||||
* @param string $uuid the UUID of the user whose email address should be updated
|
||||
* @param string $email the new email address
|
||||
* @param Mailer $mailer the mailer to send a notification with to the user
|
||||
* @return Response a response with message `null` if the email address was updated, or a response with a message
|
||||
* explaining what went wrong otherwise
|
||||
* @return Response a satisfied `Response` with payload `null` if the email address was updated, or an unsatisfied
|
||||
* `Response` otherwise
|
||||
*/
|
||||
public function set_email(string $uuid, string $email, Mailer $mailer): Response
|
||||
public function set_email(string $uuid, string $email): Response
|
||||
{
|
||||
// Validate
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > self::MAX_EMAIL_LENGTH)
|
||||
return new Response(
|
||||
payload: ["target" => "email", "message" => "Invalid email address."],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
// Begin transaction
|
||||
$this->conn->beginTransaction();
|
||||
|
||||
|
@ -266,21 +225,22 @@ class UserManager
|
|||
$stmt->bindValue(":uuid", $uuid);
|
||||
$stmt->execute();
|
||||
$email_old = $stmt->fetch(PDO::FETCH_ASSOC)["email"];
|
||||
if (hash_equals($email_old, $email))
|
||||
if (hash_equals($email_old, $email)) {
|
||||
$this->conn->rollBack();
|
||||
return new Response(
|
||||
payload: ["target" => "email", "message" => "That is already your email address."],
|
||||
satisfied: false
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Reject update if email address was changed too recently (within, say, 5 minutes)
|
||||
|
||||
// Check if email address is already in use
|
||||
$stmt = $this->conn->prepare("SELECT COUNT(*) AS count
|
||||
FROM users
|
||||
WHERE email=:email;");
|
||||
$stmt = $this->conn->prepare("SELECT EXISTS(SELECT 1 FROM users WHERE email=:email);");
|
||||
$stmt->bindValue(":email", $email);
|
||||
$stmt->execute();
|
||||
if ($stmt->fetch(PDO::FETCH_ASSOC)["count"] > 0) {
|
||||
$result = $stmt->fetch();
|
||||
if ($result[0] === 1) {
|
||||
$this->conn->rollBack();
|
||||
return new Response(
|
||||
payload: ["target" => "email", "message" => "Email address already in use."],
|
||||
|
@ -302,7 +262,7 @@ class UserManager
|
|||
|
||||
// Respond
|
||||
$this->conn->commit();
|
||||
$mailer->queue_verification($email, $email_verification_token);
|
||||
$this->mailer->queue_verification($email, $email_verification_token);
|
||||
return new Response(payload: null, satisfied: true);
|
||||
}
|
||||
|
||||
|
@ -311,45 +271,35 @@ class UserManager
|
|||
*
|
||||
* @param string $email the email address to verify
|
||||
* @param string $email_verification_token the token to verify the email address with
|
||||
* @return Response the server's response to the request
|
||||
* @return Response a satisfied `Response` if the email address was newly verified, or an unsatisfied response if
|
||||
* the email address is unknown, already verified, or the token is incorrect
|
||||
*/
|
||||
public function verify_email(string $email, string $email_verification_token): Response
|
||||
{
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > self::MAX_EMAIL_LENGTH)
|
||||
return new Response(
|
||||
payload: ["target" => "email", "message" => "Invalid email address."],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
$this->conn->beginTransaction();
|
||||
|
||||
$stmt = $this->conn->prepare("SELECT email_verification_token
|
||||
FROM users
|
||||
WHERE email=:email;");
|
||||
$stmt = $this->conn->prepare("SELECT EXISTS(SELECT 1
|
||||
FROM users
|
||||
WHERE email=:email
|
||||
AND email_verification_token=:email_verification_token);");
|
||||
$stmt->bindValue(":email", $email);
|
||||
$stmt->bindValue(":email_verification_token", $email_verification_token);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// TODO: Reformat the following line
|
||||
if ($result === false ||
|
||||
($result["email_verification_token"] !== null
|
||||
&& !hash_equals($result["email_verification_token"], $email_verification_token))) {
|
||||
$result = $stmt->fetch();
|
||||
if ($result[0] !== 1) {
|
||||
$this->conn->rollBack();
|
||||
return new Response(
|
||||
payload: ["target" => "email", "message" => "Failed to verify email address."],
|
||||
payload: [
|
||||
"target" => "email",
|
||||
"message" =>
|
||||
"Failed to verify email address. " .
|
||||
"Perhaps this email address has already been verified."
|
||||
],
|
||||
satisfied: false
|
||||
);
|
||||
}
|
||||
|
||||
if ($result["email_verification_token"] === null) {
|
||||
// Email already verified, returning
|
||||
$this->conn->rollBack();
|
||||
return new Response(payload: null, satisfied: true);
|
||||
}
|
||||
|
||||
$stmt = $this->conn->prepare("UPDATE users
|
||||
SET email_verification_token=null
|
||||
WHERE email=:email;");
|
||||
$stmt = $this->conn->prepare("UPDATE users SET email_verification_token=null WHERE email=:email;");
|
||||
$stmt->bindValue(":email", $email);
|
||||
$stmt->execute();
|
||||
|
||||
|
@ -361,11 +311,10 @@ class UserManager
|
|||
* Resends the email verification email.
|
||||
*
|
||||
* @param string $uuid the UUID of the user to resend the email for
|
||||
* @param Mailer $mailer the mailer to send the email with to the user
|
||||
* @return Response a response with message `null` if the email was sent, or a response with a message explaining
|
||||
* what went wrong otherwise
|
||||
* @return Response a satisfied `Response` with payload `null` if the email was sent, or an unsatisfied `Response`
|
||||
* otherwise
|
||||
*/
|
||||
public function resend_verify_email(string $uuid, Mailer $mailer): Response
|
||||
public function resend_verify_email(string $uuid): Response
|
||||
{
|
||||
$this->conn->beginTransaction();
|
||||
|
||||
|
@ -375,7 +324,6 @@ class UserManager
|
|||
$stmt->bindValue(":uuid", $uuid);
|
||||
$stmt->execute();
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!isset($user["email_verification_token"])) {
|
||||
$this->conn->rollBack();
|
||||
return new Response(
|
||||
|
@ -400,13 +348,12 @@ class UserManager
|
|||
);
|
||||
}
|
||||
|
||||
$mailer->queue_verification($user["email"], $user["email_verification_token"]);
|
||||
|
||||
$stmt = $this->conn->prepare("UPDATE users
|
||||
SET email_verification_token_timestamp=unixepoch()
|
||||
WHERE uuid=:uuid;");
|
||||
$stmt->bindValue(":uuid", $uuid);
|
||||
$stmt->execute();
|
||||
$this->mailer->queue_verification($user["email"], $user["email_verification_token"]);
|
||||
|
||||
$this->conn->commit();
|
||||
return new Response(payload: null, satisfied: true);
|
||||
|
@ -419,26 +366,12 @@ class UserManager
|
|||
* @param string $password_old the old password
|
||||
* @param string $password_new the new password
|
||||
* @param string $password_confirm the confirmation of the new password
|
||||
* @return Response a response with message `null` if the password was updated, or a response with a message
|
||||
* explaining what went wrong otherwise
|
||||
* @return Response a satisfied `Response` with payload `null` if the password was updated, or an unsatisfied
|
||||
* `Response` otherwise
|
||||
*/
|
||||
public function set_password(string $uuid, string $password_old, string $password_new,
|
||||
string $password_confirm): Response
|
||||
{
|
||||
// Validate
|
||||
if (strlen($password_old) > self::MAX_PASSWORD_LENGTH)
|
||||
return new Response(
|
||||
payload: ["target" => "passwordOld", "message" => "Incorrect old password."],
|
||||
satisfied: false
|
||||
);
|
||||
if (strlen($password_new) < self::MIN_PASSWORD_LENGTH || strlen($password_new) > self::MAX_PASSWORD_LENGTH)
|
||||
return new Response(
|
||||
payload: [
|
||||
"target" => "passwordNew",
|
||||
"message" => "Your password should be at least 8 and at most 64 characters long."
|
||||
],
|
||||
satisfied: false
|
||||
);
|
||||
if ($password_new !== $password_confirm)
|
||||
return new Response(
|
||||
payload: ["target" => "passwordConfirm", "message" => "New passwords do not match."],
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace php;
|
||||
|
||||
use Exception;
|
||||
use Monolog\ErrorHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
|
||||
|
||||
/**
|
||||
* Helper functions.
|
||||
*/
|
||||
class Util
|
||||
{
|
||||
/**
|
||||
* Sets HTTP status code and exits the script.
|
||||
*
|
||||
* @param int $status the HTTP status code to set
|
||||
* @return never-returns
|
||||
*/
|
||||
static function http_exit(int $status): void
|
||||
{
|
||||
http_response_code($status);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the configuration file and overrides it with the user's custom values.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>|null the configuration
|
||||
*/
|
||||
static function read_config(): ?array
|
||||
{
|
||||
// TODO: Check permissions, return `null` if too permissive
|
||||
$config = parse_ini_file("config.default.ini.php", process_sections: true, scanner_mode: INI_SCANNER_TYPED);
|
||||
if ($config === false) return null;
|
||||
|
||||
if (file_exists("config.ini.php")) {
|
||||
$config_custom = parse_ini_file("config.ini.php", process_sections: true, scanner_mode: INI_SCANNER_TYPED);
|
||||
if ($config_custom === false) return null;
|
||||
|
||||
$config = array_merge($config, $config_custom);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the main logger instance.
|
||||
*
|
||||
* @param array<string, mixed> $logger_config the logger section of the configuration
|
||||
* @return Logger the main logger instance
|
||||
*/
|
||||
static function create_logger(array $logger_config): Logger
|
||||
{
|
||||
$logger = new Logger("main");
|
||||
$logger->pushHandler(new StreamHandler($logger_config["file"], $logger_config["level"]));
|
||||
ErrorHandler::register($logger);
|
||||
|
||||
return $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an appropriate CSRF token.
|
||||
*
|
||||
* @param Logger $logger the logger to use if something goes wrong
|
||||
* @return string|null the CSRF token, or `null` if no CSRF token could be created
|
||||
*/
|
||||
static function generate_csrf_token(Logger $logger): ?string
|
||||
{
|
||||
try {
|
||||
return bin2hex(random_bytes(32));
|
||||
} catch (Exception $exception) {
|
||||
$logger->emergency("Failed to generate token.", ["cause" => $exception]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses POST values from JSON-based inputs.
|
||||
*
|
||||
* @return ?array<string, mixed> the parsed POST-ed values
|
||||
*/
|
||||
static function parse_post(): ?array
|
||||
{
|
||||
$output = $_POST;
|
||||
|
||||
$post_input = file_get_contents("php://input");
|
||||
if ($post_input !== false)
|
||||
$output = json_decode($post_input, associative: true);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,280 @@
|
|||
<?php
|
||||
|
||||
namespace php;
|
||||
|
||||
|
||||
/**
|
||||
* Validates arrays of inputs such as `$_POST` or `$_SESSION` using `Rule`s.
|
||||
*/
|
||||
class Validator
|
||||
{
|
||||
/**
|
||||
* Checks whether values in `inputs` match the rules specified in `rule_sets`.
|
||||
*
|
||||
* @param array<string, string> $inputs the array of inputs in which to check the values
|
||||
* @param array<string, Rule[]> $rule_sets maps keys in `inputs` to an array of `Rule`s to be checked
|
||||
* @return Response|null `null` if all rules are satisfied, or an unsatisfied `Response` otherwise
|
||||
*/
|
||||
static function validate_inputs(array $inputs, array $rule_sets): Response|null
|
||||
{
|
||||
foreach ($rule_sets as $key => $rules) {
|
||||
foreach ($rules as $rule) {
|
||||
$is_valid = $rule->check($inputs, $key);
|
||||
if ($is_valid !== null)
|
||||
return $is_valid;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A rule/constraint/assertion that should hold over an input.
|
||||
*/
|
||||
abstract class Rule
|
||||
{
|
||||
/**
|
||||
* @var string|null The message to return if the rule does not apply to some input. If `null`, the rule
|
||||
* implementation can choose an appropriate message.
|
||||
*/
|
||||
public ?string $override_message;
|
||||
|
||||
|
||||
/**
|
||||
* Instantiates a new rule.
|
||||
*
|
||||
* @param string|null $override_message the message to return if the rule does not apply to some input. If `null`,
|
||||
* the rule implementation can choose an appropriate message
|
||||
*/
|
||||
public function __construct(?string $override_message = null)
|
||||
{
|
||||
$this->override_message = $override_message;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the rule holds for `$inputs[$key]`.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param string $key the key in `inputs` of the input to check
|
||||
* @return Response|null `null` if the rule holds, or an unsatisfied `Response` otherwise
|
||||
*/
|
||||
public abstract function check(array $inputs, string $key): Response|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the input to be a valid email address.
|
||||
*/
|
||||
class EmailRule extends Rule
|
||||
{
|
||||
/**
|
||||
* Checks whether the input is a valid email address.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param string $key the key in `inputs` of the input to check
|
||||
* @return Response|null `null` if `$inputs[$key]` is an email address, or an unsatisfied `Response` otherwise
|
||||
*/
|
||||
public function check(array $inputs, string $key): Response|null
|
||||
{
|
||||
if (!isset($inputs[$key]) || !filter_var($inputs[$key], FILTER_VALIDATE_EMAIL))
|
||||
return new Response(
|
||||
payload: ["target" => $key, "message" => $this->override_message ?? "Invalid email address."],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the input to be equal to a pre-known value.
|
||||
*/
|
||||
class EqualsRule extends Rule
|
||||
{
|
||||
/**
|
||||
* @var string|null The known value to check against, or `null` if the check should always fail.
|
||||
*/
|
||||
private string|null $known;
|
||||
|
||||
|
||||
/**
|
||||
* Instantiates a new rule.
|
||||
*
|
||||
* @param string|null $known the known value to check against, or `null` if the check should always fail
|
||||
* @param string|null $override_message the message to return if the rule does not apply to some input. If `null`,
|
||||
* the rule implementation can choose an appropriate message
|
||||
*/
|
||||
public function __construct(string|null $known, ?string $override_message = null)
|
||||
{
|
||||
parent::__construct($override_message);
|
||||
|
||||
$this->known = $known;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the input equals the known value.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param string $key the key in `inputs` of the input to check
|
||||
* @return Response|null `null` if `$this->known` is not `null` and `$inputs[$key] === $this->known`, or an
|
||||
* unsatisfied `Response` otherwise
|
||||
*/
|
||||
public function check(array $inputs, string $key): Response|null
|
||||
{
|
||||
if ($this->known === null || !isset($inputs[$key]) || !hash_equals($this->known, $inputs[$key]))
|
||||
return new Response(
|
||||
payload: [
|
||||
"target" => $key,
|
||||
"message" => $this->override_message ?? "Input '$key' does not equal known string."
|
||||
],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the input to not be blank.
|
||||
*/
|
||||
class IsNotBlankRule extends Rule
|
||||
{
|
||||
/**
|
||||
* Checks whether the input is not blank.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param string $key the key in `inputs` of the input to check
|
||||
* @return Response|null `null` if `trim($inputs[$key])` is not an empty string, or an unsatisfied `Response`
|
||||
* otherwise
|
||||
*/
|
||||
public function check(array $inputs, string $key): Response|null
|
||||
{
|
||||
if (!isset($inputs[$key]) || trim($inputs[$key]) === "")
|
||||
return new Response(
|
||||
payload: ["target" => $key, "message" => $this->override_message ?? "'$key' should not be blank."],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the input to be unset.
|
||||
*/
|
||||
class IsNotSetRule extends Rule
|
||||
{
|
||||
/**
|
||||
* Checks whether the input is unset.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param string $key the key in `inputs` of the input to check
|
||||
* @return Response|null `null` if `!isset($inputs[$key])`, or an unsatisfied `Response` otherwise
|
||||
*/
|
||||
public function check(array $inputs, string $key): Response|null
|
||||
{
|
||||
if (isset($inputs[$key]))
|
||||
return new Response(
|
||||
payload: ["target" => $key, "message" => $this->override_message ?? "Unexpected input '$key'."],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the input to be set.
|
||||
*/
|
||||
class IsSetRule extends Rule
|
||||
{
|
||||
/**
|
||||
* Checks whether the input is set.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param string $key the key in `inputs` of the input to check
|
||||
* @return Response|null `null` if `isset($inputs[$key])`, or an unsatisfied `Response` otherwise
|
||||
*/
|
||||
public function check(array $inputs, string $key): Response|null
|
||||
{
|
||||
if (!isset($inputs[$key]))
|
||||
return new Response(
|
||||
payload: ["target" => $key, "message" => $this->override_message ?? "Missing input '$key'."],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the input to be of a specific length.
|
||||
*/
|
||||
class LengthRule extends Rule
|
||||
{
|
||||
/**
|
||||
* @var int|null The minimum length (inclusive), or `null` if there is no minimum length.
|
||||
*/
|
||||
private readonly ?int $min_length;
|
||||
/**
|
||||
* @var int|null The maximum length (inclusive), or `null` if there is no maximum length.
|
||||
*/
|
||||
private readonly ?int $max_length;
|
||||
|
||||
|
||||
/**
|
||||
* Instantiates a new rule.
|
||||
*
|
||||
* @param int|null $min_length the minimum length (inclusive), or `null` if there is no minimum length
|
||||
* @param int|null $max_length the maximum length (inclusive), or `null` if there is no maximum length
|
||||
* @param string|null $override_message the message to return if the rule does not apply to some input. If `null`,
|
||||
* the rule implementation can choose an appropriate message
|
||||
*/
|
||||
public function __construct(?int $min_length = null, ?int $max_length = null, ?string $override_message = null)
|
||||
{
|
||||
parent::__construct($override_message);
|
||||
|
||||
$this->min_length = $min_length;
|
||||
$this->max_length = $max_length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the input is of the specified length.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param string $key the key in `inputs` of the input to check
|
||||
* @return Response|null `null` if the input is of the specified length, or an unsatisfied `Response` otherwise
|
||||
*/
|
||||
public function check(array $inputs, string $key): Response|null
|
||||
{
|
||||
if (!isset($inputs[$key]))
|
||||
return new Response(
|
||||
payload: ["target" => $key, "message" => $this->override_message ?? "Missing input '$key'."],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
if ($this->min_length !== null && strlen($inputs[$key]) < $this->min_length)
|
||||
return new Response(
|
||||
payload: [
|
||||
"target" => $key,
|
||||
"message" => $this->override_message ?? "'$key' should be at least $this->min_length characters."
|
||||
],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
if ($this->max_length !== null && strlen($inputs[$key]) > $this->max_length)
|
||||
return new Response(
|
||||
payload: [
|
||||
"target" => $key,
|
||||
"message" => $this->override_message ?? "'$key' should be at most $this->max_length characters."
|
||||
],
|
||||
satisfied: false
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue