Improve documentation, move classes around, etc.
This commit is contained in:
parent
4fcf615e41
commit
0cf3897f79
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "fwdekker/death-notifier",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"version": "0.15.0", "_comment_version": "Also update version in `package.json`!",
|
||||
"version": "0.15.1", "_comment_version": "Also update version in `package.json`!",
|
||||
"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.15.0", "_comment_version": "Also update version in `composer.json`!",
|
||||
"version": "0.15.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",
|
||||
|
@ -28,7 +28,7 @@
|
|||
"grunt-run": "^0.8.1",
|
||||
"grunt-text-replace": "^0.4.0",
|
||||
"grunt-webpack": "^5.0.0",
|
||||
"ts-loader": "^9.4.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.3",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.0"
|
||||
|
|
101
src/main/api.php
101
src/main/api.php
|
@ -2,11 +2,10 @@
|
|||
|
||||
use com\fwdekker\deathnotifier\ActionDispatcher;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\cli\EmulateCronAction;
|
||||
use com\fwdekker\deathnotifier\cli\ProcessEmailQueueAction;
|
||||
use com\fwdekker\deathnotifier\cli\UpdateTrackingsAction;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\EmulateCronCliAction;
|
||||
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||
use com\fwdekker\deathnotifier\mailer\ProcessEmailQueueCliAction;
|
||||
use com\fwdekker\deathnotifier\Mediawiki;
|
||||
use com\fwdekker\deathnotifier\Response;
|
||||
use com\fwdekker\deathnotifier\StartSessionAction;
|
||||
|
@ -14,6 +13,7 @@ use com\fwdekker\deathnotifier\trackings\AddTrackingAction;
|
|||
use com\fwdekker\deathnotifier\trackings\ListTrackingsAction;
|
||||
use com\fwdekker\deathnotifier\trackings\RemoveTrackingAction;
|
||||
use com\fwdekker\deathnotifier\trackings\TrackingManager;
|
||||
use com\fwdekker\deathnotifier\trackings\UpdateTrackingsCliAction;
|
||||
use com\fwdekker\deathnotifier\user\GetUserDataAction;
|
||||
use com\fwdekker\deathnotifier\user\LoginAction;
|
||||
use com\fwdekker\deathnotifier\user\LogoutAction;
|
||||
|
@ -35,7 +35,7 @@ require_once __DIR__ . "/.vendor/autoload.php";
|
|||
|
||||
|
||||
// Preamble
|
||||
$config = Util::read_config() ?? Util::http_exit(500);
|
||||
$config = Util::read_config();
|
||||
// TODO: Improve logging specificity and usefulness
|
||||
$logger = Util::create_logger($config["logger"]);
|
||||
$db = new Database($logger->withName("Database"), $config["database"]["filename"]);
|
||||
|
@ -45,54 +45,65 @@ $mediawiki = new Mediawiki($logger->withName("Mediawiki"));
|
|||
$user_manager = new UserManager($logger->withName("UserManager"), $db->conn, $mailer);
|
||||
$tracking_manager = new TrackingManager($logger->withName("TrackingManager"), $db->conn, $mailer, $mediawiki);
|
||||
|
||||
$db->auto_install($mailer, $user_manager, $tracking_manager);
|
||||
$db->auto_migrate();
|
||||
|
||||
session_start();
|
||||
$_SESSION["token"] = $_SESSION["token"] ?? Util::generate_csrf_token() ?? Util::http_exit(500);
|
||||
$_POST = Util::parse_post();
|
||||
// Handle request
|
||||
try {
|
||||
session_start();
|
||||
$_SESSION["token"] = $_SESSION["token"] ?? Util::generate_csrf_token();
|
||||
$_POST = Util::parse_post();
|
||||
|
||||
// Update database
|
||||
$db->auto_install($mailer, $user_manager, $tracking_manager);
|
||||
$db->auto_migrate();
|
||||
|
||||
// Process request
|
||||
$dispatcher = new ActionDispatcher();
|
||||
// GET actions
|
||||
$dispatcher->register_action(new StartSessionAction($user_manager));
|
||||
$dispatcher->register_action(new GetUserDataAction($user_manager));
|
||||
$dispatcher->register_action(new ListTrackingsAction($tracking_manager));
|
||||
$dispatcher->register_action(new ValidatePasswordResetTokenAction($user_manager));
|
||||
// POST actions
|
||||
$dispatcher->register_action(new RegisterAction($user_manager));
|
||||
$dispatcher->register_action(new LoginAction($user_manager));
|
||||
$dispatcher->register_action(new LogoutAction());
|
||||
$dispatcher->register_action(new ResendVerifyEmailAction($user_manager));
|
||||
$dispatcher->register_action(new VerifyEmailAction($user_manager));
|
||||
$dispatcher->register_action(new UpdateEmailAction($user_manager));
|
||||
$dispatcher->register_action(new ToggleNotificationsAction($user_manager));
|
||||
$dispatcher->register_action(new UpdatePasswordAction($user_manager));
|
||||
$dispatcher->register_action(new SendPasswordResetAction($user_manager));
|
||||
$dispatcher->register_action(new ResetPasswordAction($user_manager));
|
||||
$dispatcher->register_action(new UserDeleteAction($user_manager));
|
||||
$dispatcher->register_action(new AddTrackingAction($tracking_manager));
|
||||
$dispatcher->register_action(new RemoveTrackingAction($tracking_manager));
|
||||
// CLI actions
|
||||
$dispatcher->register_action(new UpdateTrackingsAction($config, $tracking_manager));
|
||||
$dispatcher->register_action(new ProcessEmailQueueAction($config, $mailer));
|
||||
$dispatcher->register_action(new EmulateCronAction($config, $tracking_manager, $mailer));
|
||||
// Dispatch
|
||||
if (isset($_GET["action"]))
|
||||
$response = $dispatcher->handle(ActionMethod::GET, $_GET["action"]);
|
||||
else if (isset($_POST["action"]))
|
||||
$response = $dispatcher->handle(ActionMethod::POST, $_POST["action"]);
|
||||
else if ($argc > 1)
|
||||
$response = $dispatcher->handle(ActionMethod::CLI, $argv[1]);
|
||||
else
|
||||
$response = Response::satisfied();
|
||||
// Dispatch request
|
||||
$dispatcher = new ActionDispatcher();
|
||||
// GET actions
|
||||
$dispatcher->register_action(new StartSessionAction($user_manager));
|
||||
$dispatcher->register_action(new GetUserDataAction($user_manager));
|
||||
$dispatcher->register_action(new ListTrackingsAction($tracking_manager));
|
||||
$dispatcher->register_action(new ValidatePasswordResetTokenAction($user_manager));
|
||||
// POST actions
|
||||
$dispatcher->register_action(new RegisterAction($user_manager));
|
||||
$dispatcher->register_action(new LoginAction($user_manager));
|
||||
$dispatcher->register_action(new LogoutAction());
|
||||
$dispatcher->register_action(new ResendVerifyEmailAction($user_manager));
|
||||
$dispatcher->register_action(new VerifyEmailAction($user_manager));
|
||||
$dispatcher->register_action(new UpdateEmailAction($user_manager));
|
||||
$dispatcher->register_action(new ToggleNotificationsAction($user_manager));
|
||||
$dispatcher->register_action(new UpdatePasswordAction($user_manager));
|
||||
$dispatcher->register_action(new SendPasswordResetAction($user_manager));
|
||||
$dispatcher->register_action(new ResetPasswordAction($user_manager));
|
||||
$dispatcher->register_action(new UserDeleteAction($user_manager));
|
||||
$dispatcher->register_action(new AddTrackingAction($tracking_manager));
|
||||
$dispatcher->register_action(new RemoveTrackingAction($tracking_manager));
|
||||
// CLI actions
|
||||
$cli_actions = [
|
||||
new UpdateTrackingsCliAction($config, $tracking_manager),
|
||||
new ProcessEmailQueueCliAction($config, $mailer),
|
||||
];
|
||||
$dispatcher->register_action($cli_actions[0]);
|
||||
$dispatcher->register_action($cli_actions[1]);
|
||||
$dispatcher->register_action(new EmulateCronCliAction($cli_actions));
|
||||
// Dispatch
|
||||
if (isset($_GET["action"]))
|
||||
$response = $dispatcher->handle(ActionMethod::GET);
|
||||
else if (isset($_POST["action"]))
|
||||
$response = $dispatcher->handle(ActionMethod::POST);
|
||||
else if ($argc > 1)
|
||||
$response = $dispatcher->handle(ActionMethod::CLI);
|
||||
else
|
||||
$response = Response::satisfied();
|
||||
} catch (Exception $exception) {
|
||||
$response = Response::unsatisfied("An unexpected error occurred. Please try again later.");
|
||||
$logger->error("An unexpected error occurred. Please try again later.", ["cause" => $exception]);
|
||||
}
|
||||
|
||||
|
||||
// Respond
|
||||
header("Content-type:application/json;charset=utf-8");
|
||||
exit(json_encode(array(
|
||||
exit(json_encode([
|
||||
"payload" => $response->payload,
|
||||
"satisfied" => $response->satisfied,
|
||||
"token" => $_SESSION["token"]
|
||||
)));
|
||||
]));
|
||||
|
|
|
@ -5,22 +5,56 @@ namespace com\fwdekker\deathnotifier;
|
|||
use com\fwdekker\deathnotifier\validator\IsEqualToRule;
|
||||
use com\fwdekker\deathnotifier\validator\IsNotSetRule;
|
||||
use com\fwdekker\deathnotifier\validator\IsSetRule;
|
||||
use com\fwdekker\deathnotifier\validator\Rule;
|
||||
use InvalidArgumentException;
|
||||
|
||||
|
||||
/**
|
||||
* An action that can be performed in response to a request from the user.
|
||||
*/
|
||||
abstract class Action
|
||||
{
|
||||
/**
|
||||
* @var bool `true` if and only if this action requires the user to be logged in
|
||||
*/
|
||||
private readonly bool $require_logged_in;
|
||||
/**
|
||||
* @var bool `true` if and only if this action requires the user to be logged out
|
||||
*/
|
||||
private readonly bool $require_logged_out;
|
||||
/**
|
||||
* @var bool `true` if and only if this action requires the request to have a valid CSRF token
|
||||
*/
|
||||
private readonly bool $require_valid_csrf_token;
|
||||
/**
|
||||
* @var array<string, Rule[]> maps input keys to {@see Rule}s that should be validated before this action is handled
|
||||
*/
|
||||
private readonly array $rule_lists;
|
||||
|
||||
/**
|
||||
* @var ActionMethod the method that this action handles
|
||||
*/
|
||||
public readonly ActionMethod $method;
|
||||
public readonly string $action;
|
||||
/**
|
||||
* @var string the name of the action that this action handles
|
||||
*/
|
||||
public readonly string $name;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new action.
|
||||
*
|
||||
* @param ActionMethod $method the method that this action handles
|
||||
* @param string $name the name of the action that this action handles
|
||||
* @param bool $require_logged_in `true` if and only if this action requires the user to be logged in
|
||||
* @param bool $require_logged_out `true` if and only if this action requires the user to be logged out
|
||||
* @param bool $require_valid_csrf_token `true` if and only if this action requires the request to have a valid CSRF
|
||||
* token
|
||||
* @param array<string, Rule[]> $rule_lists maps input keys to {@see Rule}s that should be validated before this
|
||||
* action is handled
|
||||
*/
|
||||
public function __construct(ActionMethod $method,
|
||||
string $action,
|
||||
string $name,
|
||||
bool $require_logged_in = false,
|
||||
bool $require_logged_out = false,
|
||||
bool $require_valid_csrf_token = false,
|
||||
|
@ -30,7 +64,7 @@ abstract class Action
|
|||
throw new InvalidArgumentException("Cannot require that user is both logged in and logged out.");
|
||||
|
||||
$this->method = $method;
|
||||
$this->action = $action;
|
||||
$this->name = $name;
|
||||
|
||||
$this->require_logged_in = $require_logged_in;
|
||||
$this->require_logged_out = $require_logged_out;
|
||||
|
@ -39,13 +73,8 @@ abstract class Action
|
|||
}
|
||||
|
||||
|
||||
final function can_handle(ActionMethod $method, string $action): bool
|
||||
{
|
||||
return $method === $this->method && $action === $this->action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates inputs, throwing an exception if any input is invalid.
|
||||
* Validates inputs according to `rule_lists`, throwing an exception if any input is invalid.
|
||||
*
|
||||
* @return void if the input is valid
|
||||
* @throws ValidationException if the input is invalid
|
||||
|
@ -59,10 +88,8 @@ abstract class Action
|
|||
if ($this->require_logged_out)
|
||||
(new IsNotSetRule("You must be logged out to perform this action."))->check($_SESSION, "uuid");
|
||||
if ($this->require_valid_csrf_token)
|
||||
(new IsEqualToRule(
|
||||
$_SESSION["token"],
|
||||
"Invalid request token. Please refresh the page and try again."
|
||||
))->check($inputs, "token");
|
||||
(new IsEqualToRule($_SESSION["token"], "Invalid request token. Please refresh the page and try again."))
|
||||
->check($inputs, "token");
|
||||
|
||||
foreach ($this->rule_lists as $key => $rule_list)
|
||||
foreach ($rule_list as $rule)
|
||||
|
|
|
@ -2,36 +2,66 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
|
||||
/**
|
||||
* Dispatches actions to the right implementation.
|
||||
*/
|
||||
class ActionDispatcher
|
||||
{
|
||||
/**
|
||||
* @var array<Action>
|
||||
* @var array<string, array<string, Action>> the registered actions
|
||||
*/
|
||||
private array $actions = array();
|
||||
private array $actions = [];
|
||||
|
||||
|
||||
/**
|
||||
* Registers `action` so that `handle` can find it.
|
||||
*
|
||||
* Only one action can be registered given a combination of method and action name. An exception is thrown if
|
||||
* this restriction is violated, because that is likely to be a mistake.
|
||||
*
|
||||
* @param Action $action the action to register
|
||||
* @return void
|
||||
*/
|
||||
public function register_action(Action $action): void
|
||||
{
|
||||
$this->actions[] = $action;
|
||||
$method = $action->method->name;
|
||||
|
||||
if (!isset($this->actions[$method]))
|
||||
$this->actions[$method] = [];
|
||||
|
||||
if (isset($this->actions[$method][$action->name]))
|
||||
// TODO: Throw more specific exceptions(?)
|
||||
throw new InvalidArgumentException("Cannot register another handler for $method action '$action->name'.");
|
||||
|
||||
$this->actions[$method][$action->name] = $action;
|
||||
}
|
||||
|
||||
public function handle(ActionMethod $method, string $action_name): Response
|
||||
/**
|
||||
* Executes the registered action for the given pair of method and action name.
|
||||
*
|
||||
* @param ActionMethod $method the method of the action to execute
|
||||
* @return Response a satisfied response with the action's output if the action did not throw an exception, or an
|
||||
* unsatisfied response with the exception's message and target
|
||||
*/
|
||||
public function handle(ActionMethod $method): Response
|
||||
{
|
||||
$suitable_actions = array_filter($this->actions, fn($action) => $action->can_handle($method, $action_name));
|
||||
if (empty($suitable_actions))
|
||||
return Response::unsatisfied("Unknown $method->name action '$action_name'.");
|
||||
if (sizeof($suitable_actions) > 1)
|
||||
return Response::unsatisfied("Multiple handlers for $method->name action '$action_name'.");
|
||||
$inputs = $method->get_inputs();
|
||||
if (!isset($inputs["action"]))
|
||||
throw new InvalidArgumentException("No action specified.");
|
||||
|
||||
$action = $suitable_actions[array_key_first($suitable_actions)];
|
||||
$action_name = $inputs["action"];
|
||||
if (!isset($this->actions[$method->name]) || !isset($this->actions[$method->name][$action_name]))
|
||||
throw new InvalidArgumentException("No handler for $method->name action '$action_name'.");
|
||||
|
||||
$action = $this->actions[$method->name][$action_name];
|
||||
try {
|
||||
$action->validate_inputs();
|
||||
$payload = $action->handle();
|
||||
return Response::satisfied($payload);
|
||||
} catch (ValidationException $exception) {
|
||||
return Response::unsatisfied($exception->getMessage(), $exception->target);
|
||||
} catch (ActionException $exception) {
|
||||
} catch (ActionException|ValidationException $exception) {
|
||||
return Response::unsatisfied($exception->getMessage(), $exception->target);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,25 @@ namespace com\fwdekker\deathnotifier;
|
|||
use Exception;
|
||||
|
||||
|
||||
/**
|
||||
* Thrown if an action could not be completed.
|
||||
*/
|
||||
class ActionException extends Exception
|
||||
{
|
||||
/**
|
||||
* @var string|null the input element that caused the exception, or `null` if no such element could be identified
|
||||
*/
|
||||
public readonly ?string $target;
|
||||
|
||||
|
||||
public function __construct(?string $message = null, ?string $target = null)
|
||||
/**
|
||||
* Constructs a new `ActionException`.
|
||||
*
|
||||
* @param string $message the message to show to the user
|
||||
* @param string|null $target the input element that caused the exception, or `null` if no such element could be
|
||||
* identified
|
||||
*/
|
||||
public function __construct(string $message, ?string $target = null)
|
||||
{
|
||||
parent::__construct($message);
|
||||
|
||||
|
|
|
@ -3,17 +3,34 @@
|
|||
namespace com\fwdekker\deathnotifier;
|
||||
|
||||
|
||||
/**
|
||||
* The method by which a user requests an action.
|
||||
*/
|
||||
enum ActionMethod
|
||||
{
|
||||
/**
|
||||
* Command-line interface.
|
||||
*/
|
||||
case CLI;
|
||||
/**
|
||||
* HTTP GET request.
|
||||
*/
|
||||
case GET;
|
||||
/**
|
||||
* HTTP POST request.
|
||||
*/
|
||||
case POST;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the user's inputs corresponding to this request method.
|
||||
*
|
||||
* @return array<int|string, mixed> the user's inputs corresponding to this request method
|
||||
*/
|
||||
function get_inputs(): array
|
||||
{
|
||||
return match ($this) {
|
||||
ActionMethod::CLI => $_SERVER["argv"],
|
||||
ActionMethod::CLI => Util::parse_cli(),
|
||||
ActionMethod::GET => $_GET,
|
||||
ActionMethod::POST => $_POST,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier;
|
||||
|
||||
use com\fwdekker\deathnotifier\validator\Rule;
|
||||
|
||||
|
||||
/**
|
||||
* An action for the CLI, which requires a valid password to be executed.
|
||||
*/
|
||||
abstract class CliAction extends Action
|
||||
{
|
||||
/**
|
||||
* @var mixed the application's configuration
|
||||
*/
|
||||
private mixed $config;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new CLI action.
|
||||
*
|
||||
* @param mixed $config the application's configuration
|
||||
* @param string $name the name of the action that this action handles
|
||||
* @param array<string, Rule[]> $rule_lists maps input keys to {@see Rule}s that should be validated before this
|
||||
* action is handled
|
||||
*/
|
||||
public function __construct(mixed $config, string $name, array $rule_lists = [])
|
||||
{
|
||||
parent::__construct(ActionMethod::CLI, $name, rule_lists: $rule_lists);
|
||||
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates the admin password, and then validates remaining inputs as with any {@see Action}.
|
||||
*
|
||||
* @return void
|
||||
* @throws ValidationException if the input is invalid
|
||||
*/
|
||||
public function validate_inputs(): void
|
||||
{
|
||||
$inputs = $this->method->get_inputs();
|
||||
if (hash_equals($this->config["admin"]["cli_secret"], "REPLACE THIS WITH A SECRET VALUE"))
|
||||
throw new ValidationException("Default config value for 'cli_secret' detected. CLI access disabled.");
|
||||
if (!isset($inputs["password"]))
|
||||
throw new ValidationException("Password required. Specify a password using `password=...`.");
|
||||
// TODO: Read input password from file specified as an input argument in `$argv` (= `$inputs`)
|
||||
if (!hash_equals($this->config["admin"]["cli_secret"], $inputs["password"]))
|
||||
throw new ValidationException("Incorrect password.");
|
||||
|
||||
parent::validate_inputs();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier;
|
||||
|
||||
|
||||
/**
|
||||
* Periodically executes several other actions, as if cron jobs have been set up to do so.
|
||||
*
|
||||
* Intended for local development only.
|
||||
*/
|
||||
class EmulateCronCliAction extends Action
|
||||
{
|
||||
/**
|
||||
* The number of seconds between executing tasks.
|
||||
*/
|
||||
private const INTERVAL = 15;
|
||||
|
||||
/**
|
||||
* @var Action[] the actions to execute at an interval
|
||||
*/
|
||||
private readonly array $actions;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new `EmulateCronAction`.
|
||||
*
|
||||
* @param Action[] $actions the actions to execute at an interval
|
||||
*/
|
||||
public function __construct(array $actions)
|
||||
{
|
||||
parent::__construct(ActionMethod::CLI, "emulate-cron");
|
||||
|
||||
$this->actions = $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the inputs of each registered action.
|
||||
*
|
||||
* @return void if the input is valid
|
||||
* @throws ValidationException if the input is invalid
|
||||
*/
|
||||
public function validate_inputs(): void
|
||||
{
|
||||
foreach ($this->actions as $action)
|
||||
$action->validate_inputs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all trackings and processes the mail queue at a regular interval.
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
public function handle(): never
|
||||
{
|
||||
while (true) {
|
||||
print("Emulating cron jobs.\n");
|
||||
foreach ($this->actions as $action)
|
||||
$action->handle();
|
||||
print("Done\n");
|
||||
|
||||
sleep(self::INTERVAL);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -114,6 +114,7 @@ class Mediawiki
|
|||
*
|
||||
* @param array<string, string> $params the parameters to include in each query
|
||||
* @param string[] $titles the titles to query
|
||||
* @param string|null $continue_name the name of the continue parameter used for this request by the API
|
||||
* @return QueryOutput<mixed> the API's response
|
||||
* @throws Exception if the query fails
|
||||
*/
|
||||
|
|
|
@ -6,11 +6,22 @@ use com\fwdekker\deathnotifier\user\UserManager;
|
|||
use Exception;
|
||||
|
||||
|
||||
/**
|
||||
* Starts a new user session, or continues an existing one.
|
||||
*/
|
||||
class StartSessionAction extends Action
|
||||
{
|
||||
/**
|
||||
* @var UserManager the manager to validate the session through
|
||||
*/
|
||||
private readonly UserManager $user_manager;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new `StartSessionAction`.
|
||||
*
|
||||
* @param UserManager $user_manager the manager to validate the session through
|
||||
*/
|
||||
public function __construct(UserManager $user_manager)
|
||||
{
|
||||
parent::__construct(ActionMethod::GET, "start-session");
|
||||
|
@ -19,6 +30,13 @@ class StartSessionAction extends Action
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts a new user session, or continues an existing one.
|
||||
*
|
||||
* @return array{"logged_in": bool, "global_message"?: string} whether the user is logged in, and the message to be
|
||||
* displayed at the top of the page, if any
|
||||
* @throws ActionException if no CSRF token could be generated
|
||||
*/
|
||||
function handle(): array
|
||||
{
|
||||
$payload = [];
|
||||
|
@ -26,7 +44,9 @@ class StartSessionAction extends Action
|
|||
// Check if user is logged in
|
||||
if (!isset($_SESSION["uuid"])) {
|
||||
$payload["logged_in"] = false;
|
||||
} else if (!$this->user_manager->user_exists($_SESSION["uuid"])) {
|
||||
} else if ($this->user_manager->user_exists($_SESSION["uuid"])) {
|
||||
$payload["logged_in"] = true;
|
||||
} else {
|
||||
// User account was deleted
|
||||
session_destroy();
|
||||
session_start();
|
||||
|
@ -37,13 +57,11 @@ class StartSessionAction extends Action
|
|||
}
|
||||
|
||||
$payload["logged_in"] = false;
|
||||
} else {
|
||||
$payload["logged_in"] = true;
|
||||
}
|
||||
|
||||
// Read global message
|
||||
if (isset($config["server"]["global_message"]) && trim($config["server"]["global_message"]) !== "")
|
||||
$payload["global_message"] = trim($config["server"]["global_message"]);
|
||||
if (isset($this->config["server"]["global_message"]) && trim($this->config["server"]["global_message"]) !== "")
|
||||
$payload["global_message"] = trim($this->config["server"]["global_message"]);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace com\fwdekker\deathnotifier;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Monolog\ErrorHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
|
@ -29,17 +30,17 @@ class Util
|
|||
/**
|
||||
* Reads the configuration file and overrides it with the user's custom values.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>|null the configuration
|
||||
* @return array<string, array<string, mixed>> the configuration
|
||||
*/
|
||||
static function read_config(): ?array
|
||||
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 ($config === false) throw new InvalidArgumentException("Invalid `config.default.ini.php` file.");
|
||||
|
||||
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;
|
||||
if ($config_custom === false) throw new InvalidArgumentException("Invalid `config.ini.php` file.");
|
||||
|
||||
$config = array_replace_recursive($config, $config_custom);
|
||||
}
|
||||
|
@ -65,9 +66,9 @@ class Util
|
|||
/**
|
||||
* Parses POST values from JSON-based inputs.
|
||||
*
|
||||
* @return array<string, mixed>|null the parsed POST-ed values
|
||||
* @return array<int|string, mixed> the parsed POST-ed values
|
||||
*/
|
||||
static function parse_post(): ?array
|
||||
static function parse_post(): array
|
||||
{
|
||||
$output = $_POST;
|
||||
|
||||
|
@ -78,6 +79,20 @@ class Util
|
|||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses `$argv` into an array, similar to `$_GET`.
|
||||
*
|
||||
* Code from https://www.php.net/manual/en/features.commandline.php#108883.
|
||||
*
|
||||
* @return array<int|string, mixed> the parsed CLI inputs
|
||||
*/
|
||||
static function parse_cli(): array
|
||||
{
|
||||
parse_str(implode("&", array_slice($_SERVER["argv"], 1)), $output);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates an appropriate CSRF token.
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\cli;
|
||||
|
||||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\ValidationException;
|
||||
|
||||
|
||||
abstract class CliAction extends Action
|
||||
{
|
||||
private mixed $config;
|
||||
|
||||
|
||||
public function __construct(mixed $config, string $action, array $rule_lists = [])
|
||||
{
|
||||
parent::__construct(ActionMethod::CLI, $action, rule_lists: $rule_lists);
|
||||
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
|
||||
public function validate_inputs(): void
|
||||
{
|
||||
$inputs = $this->method->get_inputs();
|
||||
// TODO: Read secret from file specified from `inputs`
|
||||
if (hash_equals($this->config["admin"]["cli_secret"], "REPLACE THIS WITH A SECRET VALUE"))
|
||||
throw new ValidationException("Default value for 'cli_secret' detected. Feature disabled.");
|
||||
if (!hash_equals($this->config["admin"]["cli_secret"], $inputs[2]))
|
||||
throw new ValidationException("Incorrect value for 'cli_secret'.");
|
||||
|
||||
parent::validate_inputs();
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\cli;
|
||||
|
||||
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||
use com\fwdekker\deathnotifier\trackings\TrackingManager;
|
||||
|
||||
|
||||
class EmulateCronAction extends CliAction
|
||||
{
|
||||
private readonly TrackingManager $tracking_manager;
|
||||
private readonly Mailer $mailer;
|
||||
|
||||
|
||||
public function __construct(mixed $config, TrackingManager $tracking_manager, Mailer $mailer)
|
||||
{
|
||||
parent::__construct($config, "update-trackings");
|
||||
|
||||
$this->tracking_manager = $tracking_manager;
|
||||
$this->mailer = $mailer;
|
||||
}
|
||||
|
||||
|
||||
public function handle(): string
|
||||
{
|
||||
while (true) {
|
||||
print("Updating all trackings\n");
|
||||
$this->tracking_manager->update_trackings($this->tracking_manager->list_all_unique_person_names());
|
||||
print("Processing email queue\n");
|
||||
$this->mailer->process_queue();
|
||||
print("Done\n");
|
||||
sleep(15);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\cli;
|
||||
|
||||
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||
|
||||
|
||||
class ProcessEmailQueueAction extends CliAction
|
||||
{
|
||||
private readonly Mailer $mailer;
|
||||
|
||||
|
||||
public function __construct(mixed $config, Mailer $mailer)
|
||||
{
|
||||
parent::__construct($config, "process-email-queue");
|
||||
|
||||
$this->mailer = $mailer;
|
||||
}
|
||||
|
||||
|
||||
public function handle(): string
|
||||
{
|
||||
$this->mailer->process_queue();
|
||||
|
||||
return "Successfully processed email queue.";
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\cli;
|
||||
|
||||
use com\fwdekker\deathnotifier\trackings\TrackingManager;
|
||||
|
||||
|
||||
class UpdateTrackingsAction extends CliAction
|
||||
{
|
||||
private readonly TrackingManager $tracking_manager;
|
||||
|
||||
|
||||
public function __construct(mixed $config, TrackingManager $tracking_manager)
|
||||
{
|
||||
parent::__construct($config, "update-trackings");
|
||||
|
||||
$this->tracking_manager = $tracking_manager;
|
||||
}
|
||||
|
||||
|
||||
public function handle(): string
|
||||
{
|
||||
$this->tracking_manager->update_trackings($this->tracking_manager->list_all_unique_person_names());
|
||||
|
||||
return "Successfully updated all trackings.";
|
||||
}
|
||||
}
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\mailer;
|
||||
|
||||
use com\fwdekker\deathnotifier\trackings\NotifyArticleDeletedEmail;
|
||||
use com\fwdekker\deathnotifier\trackings\NotifyArticleUndeletedEmail;
|
||||
use com\fwdekker\deathnotifier\trackings\NotifyStatusChangedEmail;
|
||||
use com\fwdekker\deathnotifier\user\ChangedEmailEmail;
|
||||
use com\fwdekker\deathnotifier\user\ChangedPasswordEmail;
|
||||
use com\fwdekker\deathnotifier\user\RegisterEmail;
|
||||
use com\fwdekker\deathnotifier\user\ResetPasswordEmail;
|
||||
use com\fwdekker\deathnotifier\user\VerifyEmailEmail;
|
||||
use Exception;
|
||||
|
||||
|
||||
|
@ -61,6 +69,8 @@ abstract class Email
|
|||
*/
|
||||
public static function deserialize(string $type, string $recipient, string $arg1, string $arg2): Email
|
||||
{
|
||||
// TODO: Dynamically instantiate class from class name
|
||||
// TODO: Add serialize and deserialize functions, implemented by each subclass
|
||||
return match ($type) {
|
||||
RegisterEmail::TYPE => new RegisterEmail($recipient, $arg1),
|
||||
VerifyEmailEmail::TYPE => new VerifyEmailEmail($recipient, $arg1),
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\mailer;
|
||||
|
||||
use com\fwdekker\deathnotifier\CliAction;
|
||||
|
||||
|
||||
/**
|
||||
* Processes the queue of emails to send.
|
||||
*/
|
||||
class ProcessEmailQueueCliAction extends CliAction
|
||||
{
|
||||
/**
|
||||
* @var Mailer the mailer to process the queue with
|
||||
*/
|
||||
private readonly Mailer $mailer;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new `ProcessEmailQueueAction`.
|
||||
*
|
||||
* @param mixed $config the application's configuration
|
||||
* @param Mailer $mailer the mailer to process the queue with
|
||||
*/
|
||||
public function __construct(mixed $config, Mailer $mailer)
|
||||
{
|
||||
parent::__construct($config, "process-email-queue");
|
||||
|
||||
$this->mailer = $mailer;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes the queue.
|
||||
*
|
||||
* @return mixed `null`
|
||||
*/
|
||||
public function handle(): mixed
|
||||
{
|
||||
$this->mailer->process_queue();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -9,11 +9,19 @@ use com\fwdekker\deathnotifier\validator\HasLengthRule;
|
|||
use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
|
||||
|
||||
|
||||
/**
|
||||
* Adds a tracking.
|
||||
*/
|
||||
class AddTrackingAction extends Action
|
||||
{
|
||||
private readonly TrackingManager $tracking_manager;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new `AddTrackingAction`.
|
||||
*
|
||||
* @param TrackingManager $tracking_manager the manager to add the tracking to
|
||||
*/
|
||||
public function __construct(TrackingManager $tracking_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\mailer;
|
||||
namespace com\fwdekker\deathnotifier\trackings;
|
||||
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
|
||||
|
||||
/**
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\mailer;
|
||||
namespace com\fwdekker\deathnotifier\trackings;
|
||||
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
|
||||
|
||||
/**
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\mailer;
|
||||
namespace com\fwdekker\deathnotifier\trackings;
|
||||
|
||||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
|
||||
|
||||
/**
|
|
@ -5,9 +5,6 @@ namespace com\fwdekker\deathnotifier\trackings;
|
|||
use com\fwdekker\deathnotifier\ArticleType;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||
use com\fwdekker\deathnotifier\mailer\NotifyArticleDeletedEmail;
|
||||
use com\fwdekker\deathnotifier\mailer\NotifyArticleUndeletedEmail;
|
||||
use com\fwdekker\deathnotifier\mailer\NotifyStatusChangedEmail;
|
||||
use com\fwdekker\deathnotifier\Mediawiki;
|
||||
use com\fwdekker\deathnotifier\Response;
|
||||
use Exception;
|
||||
|
@ -344,7 +341,7 @@ class TrackingManager
|
|||
/**
|
||||
* Updates peoples' statuses.
|
||||
*
|
||||
* @param array<string, array{"status": \com\fwdekker\deathnotifier\trackings\PersonStatus|null, "type": ArticleType}> $statuses the current statuses of
|
||||
* @param array<string, array{"status": PersonStatus|null, "type": ArticleType}> $statuses the current statuses of
|
||||
* people
|
||||
* @return void
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\trackings;
|
||||
|
||||
use com\fwdekker\deathnotifier\CliAction;
|
||||
|
||||
|
||||
/**
|
||||
* Updates all trackings that users have added.
|
||||
*/
|
||||
class UpdateTrackingsCliAction extends CliAction
|
||||
{
|
||||
/**
|
||||
* @var TrackingManager the manager through which trackings should be updated
|
||||
*/
|
||||
private readonly TrackingManager $tracking_manager;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new `UpdateTrackingsAction`.
|
||||
*
|
||||
* @param mixed $config the application's configuration
|
||||
* @param TrackingManager $tracking_manager the manager through which trackings should be updated
|
||||
*/
|
||||
public function __construct(mixed $config, TrackingManager $tracking_manager)
|
||||
{
|
||||
parent::__construct($config, "update-trackings");
|
||||
|
||||
$this->tracking_manager = $tracking_manager;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates all trackings that users have added.
|
||||
*
|
||||
* @return mixed `null`
|
||||
*/
|
||||
public function handle(): mixed
|
||||
{
|
||||
$this->tracking_manager->update_trackings($this->tracking_manager->list_all_unique_person_names());
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ class HasLengthRule extends Rule
|
|||
/**
|
||||
* Verifies that the input is of the specific length.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param array<int|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 void if `$inputs[$key]` is of the specified length
|
||||
* @throws ValidationException if `$inputs[$key]` is not set or is not of the specified length
|
||||
|
|
|
@ -13,7 +13,7 @@ class IsEmailRule extends Rule
|
|||
/**
|
||||
* Verifies that the input is an email address.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param array<int|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 void if `$inputs[$key]` is an email address
|
||||
* @throws ValidationException if `$inputs[$key]` is not set or is not an email address
|
||||
|
|
|
@ -32,7 +32,7 @@ class IsEqualToRule extends Rule
|
|||
/**
|
||||
* Verifies that the input has the specified value.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param array<int|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 void if `$inputs[$key]` equals `$expected`
|
||||
* @throws ValidationException if `$inputs[$key]` is not set or does not equal `$expected`
|
||||
|
|
|
@ -13,7 +13,7 @@ class IsNotBlankRule extends Rule
|
|||
/**
|
||||
* Verifies that the input is not blank.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param array<int|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 void if `trim($inputs[$key])` is not an empty string
|
||||
* @throws ValidationException if `$inputs[$key]` is not set or if `trim($inputs[$key])` is an empty string
|
||||
|
|
|
@ -13,7 +13,7 @@ class IsNotSetRule extends Rule
|
|||
/**
|
||||
* Verifies that the input is not set.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param array<int|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 void if `$inputs[$key]` is not set
|
||||
* @throws ValidationException if `$inputs[$key]` is set
|
||||
|
|
|
@ -13,7 +13,7 @@ class IsSetRule extends Rule
|
|||
/**
|
||||
* Verifies that the input is set.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param array<int|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 void if `$inputs[$key]` is set
|
||||
* @throws ValidationException if `$inputs[$key]` is not set
|
||||
|
|
|
@ -35,7 +35,7 @@ abstract class Rule
|
|||
*
|
||||
* Implementations should never assume that the `$inputs[$key]` is set.
|
||||
*
|
||||
* @param array<string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param array<int|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 void if the rule holds
|
||||
* @throws ValidationException if the rule does not hold
|
||||
|
|
|
@ -5,12 +5,25 @@ namespace com\fwdekker\deathnotifier;
|
|||
use Exception;
|
||||
|
||||
|
||||
/**
|
||||
* Thrown if an action could not be handled because an input is invalid.
|
||||
*/
|
||||
class ValidationException extends Exception
|
||||
{
|
||||
/**
|
||||
* @var string|null the input element that caused the exception, or `null` if no such element could be identified
|
||||
*/
|
||||
public readonly ?string $target;
|
||||
|
||||
|
||||
public function __construct(?string $message = null, ?string $target = null)
|
||||
/**
|
||||
* Constructs a new `ValidationException`.
|
||||
*
|
||||
* @param string $message the message to show to the user
|
||||
* @param string|null $target the input element that caused the exception, or `null` if no such element could be
|
||||
* identified
|
||||
*/
|
||||
public function __construct(string $message, ?string $target = null)
|
||||
{
|
||||
parent::__construct($message);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace com\fwdekker\deathnotifier;
|
|||
|
||||
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||
use com\fwdekker\deathnotifier\trackings\TrackingManager;
|
||||
use com\fwdekker\deathnotifier\user\UserManager;
|
||||
use Exception;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Test\TestCase;
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier;
|
||||
|
||||
use com\fwdekker\deathnotifier\mailer\ChangedEmailEmail;
|
||||
use com\fwdekker\deathnotifier\mailer\ChangedPasswordEmail;
|
||||
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||
use com\fwdekker\deathnotifier\mailer\RegisterEmail;
|
||||
use com\fwdekker\deathnotifier\mailer\ResetPasswordEmail;
|
||||
use com\fwdekker\deathnotifier\mailer\VerifyEmailEmail;
|
||||
use com\fwdekker\deathnotifier\user\ChangedEmailEmail;
|
||||
use com\fwdekker\deathnotifier\user\ChangedPasswordEmail;
|
||||
use com\fwdekker\deathnotifier\user\RegisterEmail;
|
||||
use com\fwdekker\deathnotifier\user\ResetPasswordEmail;
|
||||
use com\fwdekker\deathnotifier\user\UserManager;
|
||||
use com\fwdekker\deathnotifier\user\VerifyEmailEmail;
|
||||
use PDO;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
|
|
Loading…
Reference in New Issue