diff --git a/composer.json b/composer.json index 21aa031..d46a51b 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "fwdekker/death-notifier", "description": "Get notified when a famous person dies.", - "version": "0.0.9", + "version": "0.0.10", "type": "project", "license": "MIT", "homepage": "https://git.fwdekker.com/tools/death-notifier", diff --git a/composer.lock b/composer.lock index 130c321..0189afe 100644 Binary files a/composer.lock and b/composer.lock differ diff --git a/package.json b/package.json index a6ce36e..1fada26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "death-notifier", - "version": "0.0.9", + "version": "0.0.10", "description": "Get notified when a famous person dies.", "author": "Florine W. Dekker", "browser": "dist/bundle.js", diff --git a/src/main/api.php b/src/main/api.php index 451f50b..acddc52 100644 --- a/src/main/api.php +++ b/src/main/api.php @@ -128,7 +128,7 @@ function validate_has_arguments(mixed ...$arguments): ?Response $response = null; if (isset($_POST["action"])) { - // Process POST + // POST requests; alter state switch ($_POST["action"]) { case "register": $response = validate_csrf() @@ -195,17 +195,33 @@ if (isset($_POST["action"])) { $response = new Response("Unknown POST action '" . $_POST["action"] . "'.", false); break; } -} else if (isset($_GET["action"])) { +} elseif (isset($_GET["action"])) { + // GET requests; do not alter state $response = match ($_GET["action"]) { "get-user-data" => validate_logged_in() ?? $user_manager->get_user_data($_SESSION["uuid"]), "list-trackings" => validate_logged_in() ?? $tracking_manager->list_trackings($_SESSION["uuid"]), default => new Response("Unknown GET action '" . $_GET["action"] . "'.", false), }; +} elseif ($argc > 1) { + // CLI + if (hash_equals($config["admin"]["update_secret"], "REPLACE THIS WITH A SECRET VALUE")) + exit("Default value for 'cli_secret' detected. Feature disabled."); + if (hash_equals($config["admin"]["update_secret"], $argv[2])) + exit("Incorrect value for 'cli_secret'."); + + if ($argv[1] === "update-all-trackings") { + $logger->info("Updating all trackings."); + $tracking_manager->update_trackings($tracking_manager->list_all_trackings()); + exit("Successfully updated all trackings."); + } else { + exit("Unknown CLI action '" . $argv[1] . "'."); + } } else { - $response = new Response("Unknown method.", false); + // No action given, nothing done, so that's a success + $response = new Response(null, true); } -//header("Content-type:application/json;charset=utf-8"); +header("Content-type:application/json;charset=utf-8"); exit(json_encode(array( "message" => $response->message, "satisfied" => $response->satisfied, diff --git a/src/main/config.default.ini.php b/src/main/config.default.ini.php index a048d6f..0881145 100644 --- a/src/main/config.default.ini.php +++ b/src/main/config.default.ini.php @@ -1,9 +1,19 @@ ; +[admin] +# Password to use the CLI of `api.php`. Until this value is changed from its default, the feature is disabled +cli_secret = REPLACE THIS WITH A SECRET VALUE + [database] # Relative path to SQLite database filename = .death-notifier.db +[logger] +# File to store logs in +file = .death-notifier.log +# Log level. See https://seldaek.github.io/monolog/doc/01-usage.html#log-levels +level = 300 + [mail] # Whether to enable mailing enabled = false @@ -19,9 +29,3 @@ password = TODO from_name = TODO # Email address to send test emails to to_address_test = TODO - -[logger] -# File to store logs in -file = death-notifier.log -# Log level. See https://seldaek.github.io/monolog/doc/01-usage.html#log-levels -level = 300 diff --git a/src/main/js/Main.ts b/src/main/js/Main.ts index 9f9514c..b7927d0 100644 --- a/src/main/js/Main.ts +++ b/src/main/js/Main.ts @@ -26,7 +26,7 @@ function refreshTrackings() { const trackingPersonName = document.createElement("td"); trackingPersonName.innerText = tracking["person_name"]; const trackingIsDeceased = document.createElement("td"); - trackingIsDeceased.innerText = tracking["is_deceased"] === 1 ? "yes" : "no"; + trackingIsDeceased.innerText = tracking["status"]; const trackingDelete = document.createElement("td"); const trackingDeleteButton = document.createElement("button"); trackingDeleteButton.innerText = "remove"; diff --git a/src/main/php/Database.php b/src/main/php/Database.php index 0b7f2d1..5b3cb87 100644 --- a/src/main/php/Database.php +++ b/src/main/php/Database.php @@ -18,9 +18,6 @@ class Database */ public static function connect(string $filename): PDO { - return new PDO("sqlite:" . $filename, options: array( - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_PERSISTENT => true - )); + return new PDO("sqlite:" . $filename, options: array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)); } } diff --git a/src/main/php/Mediawiki.php b/src/main/php/Mediawiki.php index b7933b9..8e84824 100644 --- a/src/main/php/Mediawiki.php +++ b/src/main/php/Mediawiki.php @@ -21,10 +21,6 @@ class Mediawiki private const USER_AGENT = "death-notifier/%%VERSION_NUMBER%% " . "(https://git.fwdekker.com/tools/death-notifier; florine@fwdekker.com)"; - /** - * Regex matching Wikipedia categories indicating deceased people. - */ - private const DECEASED_CATEGORY_REGEX = "/^Category:[0-9]{1,4} (BC |AD )?deaths$/"; /** * @var Logger The logger to use for logging. @@ -82,6 +78,7 @@ class Mediawiki "action" => "query", "format" => "json", "prop" => "info", + "redirects" => true, "titles" => implode("|", array_slice($titles, $i, 50)) ))["query"]; @@ -94,6 +91,10 @@ class Mediawiki array_map($page_exists, $response_pages) ) ); + + if (isset($response["normalized"])) + foreach ($response["normalized"] as $redirect) + $pages[$redirect["from"]] = $pages[$redirect["to"]]; } return $pages; @@ -109,38 +110,41 @@ class Mediawiki } /** - * Returns `true` if the person is deceased, `false` if the person is alive, and `null` if the person does not have - * a page on Wikipedia. + * Returns a string describing `person`'s status ("deceased", "alive", "possibly alive", "missing"), or `null` if + * the title does not refer to a page about a person on Wikipedia. * * @param mixed $person_page the page as returned by the Wikipedia API - * @return bool|null `true` if the person is deceased, `false` if the person is alive, and `null` if the person does - * not have a page on Wikipedia + * @return string|null a string describing `person`'s status ("deceased", "alive", "possibly alive", "missing"), or + * `null` if the title does not refer to a page about a person on Wikipedia */ - private function person_is_deceased(mixed $person_page): ?bool + private function person_status(mixed $person_page): ?string { if (array_key_exists("missing", $person_page) || array_key_exists("invalid", $person_page)) return null; - // TODO: Detect missing people, detect presumed dead people - $is_alive = in_array("Category:Living people", array_column($person_page["categories"], "title")); - $is_deceased = !empty(array_filter( - $person_page["categories"], - fn($it) => preg_match(self::DECEASED_CATEGORY_REGEX, $it["title"]) - )); - if (!$is_alive && !$is_deceased) - return null; + $category_titles = array_column($person_page["categories"], "title"); + $deceased_regex = "/^Category:([0-9]{1,4}s? (BC |AD )?deaths|Year of death (missing|unknown))$/"; - return $is_deceased; + if (!empty(array_filter($category_titles, fn($it) => preg_match($deceased_regex, $it)))) + return "deceased"; + elseif (in_array("Category:Possibly living people", $category_titles)) + return "possibly alive"; + elseif (in_array("Category:Missing people", $category_titles)) + return "missing"; + elseif (in_array("Category:Living people", $category_titles)) + return "alive"; + else + return null; } /** - * Checks for each person whether they are alive according to Wikipedia's categorization. + * Checks for each person what their status (dead, alive, missing) is according to Wikipedia's categorization. * * @param array $people_names the names of the people to check aliveness of - * @return array maps each requested person's name to a boolean indicating whether they are deceased. - * If a page does not exist, it is mapped to a `null` + * @return array maps each requested person's name to a string describing their status ("deceased", + * "alive", "possibly alive", "missing"). If a page does not exist, it is mapped to `null` */ - public function people_are_deceased(array $people_names): array + public function people_statuses(array $people_names): array { try { $pages = []; @@ -161,7 +165,7 @@ class Mediawiki $pages, array_combine( array_column($response_pages, "title"), - array_map(fn($it) => $this->person_is_deceased($it), $response_pages) + array_map(fn($it) => $this->person_status($it), $response_pages) ) ); diff --git a/src/main/php/TrackingManager.php b/src/main/php/TrackingManager.php index 3132be6..6a1211e 100644 --- a/src/main/php/TrackingManager.php +++ b/src/main/php/TrackingManager.php @@ -52,7 +52,7 @@ class TrackingManager public function install(): void { $conn = Database::connect($this->db_filename); - $conn->exec("CREATE TABLE trackings(user_uuid text not null, person_name text not null, is_deceased int not null default 0, PRIMARY KEY (user_uuid, person_name));"); + $conn->exec("CREATE TABLE trackings(user_uuid text not null, person_name text not null, status text not null, PRIMARY KEY (user_uuid, person_name));"); } /** @@ -69,8 +69,8 @@ class TrackingManager if (!$this->mediawiki->pages_exist([$person_name])[$person_name]) return new Response("Page does not exist.", false); - $is_deceased = $this->mediawiki->people_are_deceased([$person_name])[$person_name]; - if ($is_deceased === null) + $status = $this->mediawiki->people_statuses([$person_name])[$person_name]; + if ($status === null) return new Response("Page does not refer to a person.", false); $conn = Database::connect($this->db_filename); @@ -85,10 +85,10 @@ class TrackingManager return new Response("Tracking already exists.", false); } - $stmt = $conn->prepare("INSERT INTO trackings (user_uuid, person_name, is_deceased) VALUES (:user_uuid, :person_name, :is_deceased);"); + $stmt = $conn->prepare("INSERT INTO trackings (user_uuid, person_name, status) VALUES (:user_uuid, :person_name, :status);"); $stmt->bindValue(":user_uuid", $user_uuid); $stmt->bindValue(":person_name", $person_name); - $stmt->bindValue(":is_deceased", $is_deceased); + $stmt->bindValue(":status", $status); $stmt->execute(); $conn->commit(); @@ -125,7 +125,7 @@ class TrackingManager public function list_trackings(string $user_uuid): Response { $conn = Database::connect($this->db_filename); - $stmt = $conn->prepare("SELECT person_name, is_deceased FROM trackings WHERE user_uuid=:user_uuid;"); + $stmt = $conn->prepare("SELECT person_name, status FROM trackings WHERE user_uuid=:user_uuid;"); $stmt->bindValue(":user_uuid", $user_uuid); $stmt->execute(); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -155,13 +155,13 @@ class TrackingManager public function update_trackings(array $people_names): void { // TODO: Handle removed pages - $people_statuses = $this->mediawiki->people_are_deceased($people_names); + $people_statuses = $this->mediawiki->people_statuses($people_names); $conn = Database::connect($this->db_filename); - $stmt = $conn->prepare("UPDATE trackings SET is_deceased=:is_deceased WHERE person_name=:person_name;"); - $stmt->bindParam(":is_deceased", $person_is_deceased); + $stmt = $conn->prepare("UPDATE trackings SET status=:status WHERE person_name=:person_name;"); + $stmt->bindParam(":status", $person_status); $stmt->bindParam(":person_name", $person_name); - foreach ($people_statuses as $person_name => $person_is_deceased) + foreach ($people_statuses as $person_name => $person_status) $stmt->execute(); } }