256 lines
9.5 KiB
PHP
256 lines
9.5 KiB
PHP
<?php
|
|
|
|
namespace php;
|
|
|
|
use PDO;
|
|
|
|
|
|
/**
|
|
* Manages interaction with the database in the context of trackings.
|
|
*/
|
|
class TrackingManager
|
|
{
|
|
/**
|
|
* The minimum length of a Wikipedia page title.
|
|
*/
|
|
public const MIN_TITLE_LENGTH = 1;
|
|
/**
|
|
* The maximum length of a Wikipedia page title.
|
|
*/
|
|
public const MAX_TITLE_LENGTH = 255;
|
|
|
|
/**
|
|
* @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(PDO $conn, Mailer $mailer, Mediawiki $mediawiki)
|
|
{
|
|
$this->conn = $conn;
|
|
$this->mailer = $mailer;
|
|
$this->mediawiki = $mediawiki;
|
|
}
|
|
|
|
|
|
/**
|
|
* Populates the database with the necessary structures for users.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function install(): void
|
|
{
|
|
$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
|
|
ON UPDATE CASCADE,
|
|
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 DEFAULT '',
|
|
is_deleted INT NOT NULL DEFAULT 0);");
|
|
$this->conn->exec("CREATE TRIGGER people_cull_orphans
|
|
AFTER DELETE ON trackings
|
|
FOR EACH ROW
|
|
WHEN (SELECT COUNT(*) FROM trackings WHERE person_name=OLD.person_name)=0
|
|
BEGIN
|
|
DELETE FROM people WHERE name=OLD.person_name;
|
|
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 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
|
|
{
|
|
$pages_exist = $this->mediawiki->pages_exist([$person_name]);
|
|
$normalized_name = $pages_exist->redirects[$person_name];
|
|
if (in_array($normalized_name, $pages_exist->missing))
|
|
return new Response(
|
|
payload: ["target" => "personName", "message" => "Page does not exist."],
|
|
satisfied: false
|
|
);
|
|
|
|
$statuses = $this->mediawiki->people_statuses([$normalized_name])->results;
|
|
if (!isset($statuses[$normalized_name]))
|
|
return new Response(
|
|
payload: ["target" => "personName", "message" => "Page does not refer to a person."],
|
|
satisfied: false
|
|
);
|
|
$status = $statuses[$normalized_name];
|
|
|
|
// Insert person and tracking
|
|
$this->conn->beginTransaction();
|
|
|
|
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO people (name) VALUES (:name);");
|
|
$stmt->bindValue(":name", $normalized_name);
|
|
$stmt->execute();
|
|
|
|
$stmt = $this->conn->prepare("UPDATE people SET status=:status WHERE name=:name;");
|
|
$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)
|
|
VALUES (:user_uuid, :person_name);");
|
|
$stmt->bindValue(":user_uuid", $user_uuid);
|
|
$stmt->bindValue(":person_name", $normalized_name);
|
|
$stmt->execute();
|
|
|
|
$this->conn->commit();
|
|
return new Response(payload: null, satisfied: true);
|
|
}
|
|
|
|
/**
|
|
* Removes a tracking from the database.
|
|
*
|
|
* @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 satisfied `Response` with payload `null`
|
|
*/
|
|
public function remove_tracking(string $user_uuid, string $person_name): Response
|
|
{
|
|
$stmt = $this->conn->prepare("DELETE FROM trackings
|
|
WHERE user_uuid=:user_uuid AND person_name=:person_name;");
|
|
$stmt->bindValue(":user_uuid", $user_uuid);
|
|
$stmt->bindValue(":person_name", $person_name);
|
|
$stmt->execute();
|
|
|
|
return new Response(payload: null, satisfied: true);
|
|
}
|
|
|
|
/**
|
|
* Lists all trackings of the indicated user.
|
|
*
|
|
* @param string $user_uuid the user to return the trackings of
|
|
* @return Response a satisfied `Response` with the trackings as its payload
|
|
*/
|
|
public function list_trackings(string $user_uuid): Response
|
|
{
|
|
$stmt = $this->conn->prepare("SELECT people.name, people.status, people.is_deleted
|
|
FROM trackings
|
|
INNER JOIN people
|
|
ON trackings.user_uuid=:user_uuid
|
|
AND trackings.person_name=people.name;");
|
|
$stmt->bindValue(":user_uuid", $user_uuid);
|
|
$stmt->execute();
|
|
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
return new Response(payload: $results, satisfied: true);
|
|
}
|
|
|
|
|
|
/**
|
|
* Lists all unique names being tracked in the database.
|
|
*
|
|
* @return array<string> all unique names in the database
|
|
*/
|
|
public function list_all_unique_person_names(): array
|
|
{
|
|
$stmt = $this->conn->prepare("SELECT ALL name FROM people;");
|
|
$stmt->execute();
|
|
return array_column($stmt->fetchAll(PDO::FETCH_ASSOC), "name");
|
|
}
|
|
|
|
/**
|
|
* Updates trackings for the given names.
|
|
*
|
|
* @param string[] $people_names the names of the pages to update the tracking of
|
|
* @return 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->bindParam(":old_name", $from);
|
|
$stmt->bindParam(":new_name", $to);
|
|
foreach ($people_statuses->redirects as $from => $to)
|
|
if ($from !== $to)
|
|
$stmt->execute();
|
|
|
|
// Process deletions
|
|
$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
|
|
$stmt->execute();
|
|
|
|
// Process status changes
|
|
$stmt_undelete = $this->conn->prepare("UPDATE people SET is_deleted=0 WHERE name=:name;");
|
|
$stmt_undelete->bindParam(":name", $person_name);
|
|
|
|
$stmt_status = $this->conn->prepare("UPDATE people
|
|
SET status=:status
|
|
WHERE name=:name AND status<>:status
|
|
RETURNING name;");
|
|
$stmt_status->bindParam(":status", $person_status);
|
|
$stmt_status->bindParam(":name", $person_name);
|
|
|
|
$stmt_emails = $this->conn->prepare("SELECT users.email
|
|
FROM users
|
|
LEFT JOIN trackings
|
|
WHERE trackings.person_name=:person_name
|
|
AND trackings.user_uuid=users.uuid;");
|
|
$stmt_emails->bindParam(":person_name", $person_name);
|
|
|
|
foreach ($people_statuses->results as $person_name => $person_status) {
|
|
$stmt_undelete->execute();
|
|
$stmt_status->execute();
|
|
|
|
// TODO: Write "alive again" notification
|
|
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)
|
|
$this->mailer->queue_death_notification($user_email, $person_name);
|
|
}
|
|
}
|
|
|
|
// Commit transaction
|
|
$this->conn->commit();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* The status assigned to a person.
|
|
*/
|
|
enum PersonStatus: string
|
|
{
|
|
case Alive = "alive";
|
|
case PossiblyAlive = "possibly alive";
|
|
case Missing = "missing";
|
|
case Deceased = "deceased";
|
|
}
|