Add phpstan and resolve almost all issues

This commit is contained in:
Florine W. Dekker 2022-08-14 17:47:18 +02:00
parent c5531d21cf
commit 1f3257424a
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
9 changed files with 141 additions and 28 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "fwdekker/death-notifier", "name": "fwdekker/death-notifier",
"description": "Get notified when a famous person dies.", "description": "Get notified when a famous person dies.",
"version": "0.0.4", "version": "0.0.5",
"type": "project", "type": "project",
"license": "MIT", "license": "MIT",
"homepage": "https://git.fwdekker.com/tools/death-notifier", "homepage": "https://git.fwdekker.com/tools/death-notifier",
@ -18,8 +18,11 @@
"require": { "require": {
"ext-curl": "*", "ext-curl": "*",
"ext-sqlite3": "*", "ext-sqlite3": "*",
"monolog/monolog": "^3.2.0", "monolog/monolog": "^3.2",
"phpmailer/phpmailer": "^6.6.3" "phpmailer/phpmailer": "^6.6"
},
"require-dev": {
"phpstan/phpstan": "^1.8"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

BIN
composer.lock generated

Binary file not shown.

View File

@ -1,6 +1,6 @@
{ {
"name": "death-notifier", "name": "death-notifier",
"version": "0.0.4", "version": "0.0.5",
"description": "Get notified when a famous person dies.", "description": "Get notified when a famous person dies.",
"author": "Florine W. Dekker", "author": "Florine W. Dekker",
"browser": "dist/bundle.js", "browser": "dist/bundle.js",
@ -13,7 +13,8 @@
"clean": "grunt clean", "clean": "grunt clean",
"dev": "grunt dev", "dev": "grunt dev",
"dev:server": "grunt dev:server", "dev:server": "grunt dev:server",
"deploy": "grunt deploy" "deploy": "grunt deploy",
"stan": "vendor/bin/phpstan analyse -l 8 src/main"
}, },
"devDependencies": { "devDependencies": {
"grunt": "^1.5.3", "grunt": "^1.5.3",

4
phpstan.neon Normal file
View File

@ -0,0 +1,4 @@
parameters:
excludePaths:
- src/main/config.default.ini.php
- src/main/config.ini.php

View File

@ -15,9 +15,10 @@ require_once __DIR__ . "/vendor/autoload.php";
/// Preparations /// Preparations
// Load config // Load config
$config = parse_ini_file("config.default.ini.php", process_sections: true, scanner_mode: INI_SCANNER_TYPED); $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")) { if (file_exists("config.ini.php")) {
$config_custom = parse_ini_file("config.ini.php", process_sections: true, scanner_mode: INI_SCANNER_TYPED); $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); $config = array_merge($config, $config_custom);
} }
@ -50,12 +51,12 @@ if (!file_exists($config["database"]["filename"])) {
* *
* @return string the generated CSRF token * @return string the generated CSRF token
*/ */
function generate_csrf_token(): string function generate_csrf_token(Logger $logger): string
{ {
try { try {
return bin2hex(random_bytes(32)); return bin2hex(random_bytes(32));
} catch (Exception $exception) { } catch (Exception $exception) {
$log->emergency("Failed to generate token.", [$exception]); $logger->emergency("Failed to generate token.", [$exception]);
http_response_code(500); http_response_code(500);
exit(); exit();
} }
@ -63,10 +64,11 @@ function generate_csrf_token(): string
session_start(); session_start();
if (!isset($_SESSION["token"])) if (!isset($_SESSION["token"]))
$_SESSION["token"] = generate_csrf_token(); $_SESSION["token"] = generate_csrf_token($logger);
// Read JSON from POST // Read JSON from POST
$_POST = json_decode(file_get_contents("php://input"), associative: true); $post_input = file_get_contents("php://input");
if ($post_input !== false) $_POST = json_decode($post_input, associative: true);
/// Define validation helpers /// Define validation helpers
@ -153,7 +155,7 @@ if (isset($_POST["action"])) {
session_destroy(); session_destroy();
session_start(); session_start();
$_SESSION["token"] = generate_csrf_token(); $_SESSION["token"] = generate_csrf_token($logger);
$response = new Response(null, true); $response = new Response(null, true);
break; break;
case "update-email": case "update-email":
@ -194,7 +196,7 @@ if (isset($_POST["action"])) {
$response = validate_csrf() ?? $mailer->send_test_mail(); $response = validate_csrf() ?? $mailer->send_test_mail();
break; break;
default: default:
$response["message"] = "Unknown POST action '" . $_POST["action"] . "'."; $response = new Response("Unknown POST action '" . $_POST["action"] . "'.", false);
break; break;
} }
} else if (isset($_GET["action"])) { } else if (isset($_GET["action"])) {

View File

@ -42,11 +42,11 @@ class Mediawiki
/** /**
* Sends a request to Wikipedia's API and returns its response as a JSON object. * Sends a request to Wikipedia's API and returns its response as a JSON object.
* *
* @param array $url_param the query parameters to send to the API * @param array<string, mixed> $url_param the query parameters to send to the API
* @return array a JSON object containing the API's response * @return mixed a JSON object containing the API's response
* @throws Exception if the API could not be reached * @throws Exception if the API could not be reached
*/ */
private function api_fetch(array $url_param): array private function api_fetch(array $url_param): mixed
{ {
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
@ -55,7 +55,7 @@ class Mediawiki
$output = curl_exec($ch); $output = curl_exec($ch);
curl_close($ch); curl_close($ch);
if (curl_error($ch)) if (is_bool($output) || curl_error($ch))
throw new Exception(curl_error($ch)); throw new Exception(curl_error($ch));
return json_decode($output, associative: true); return json_decode($output, associative: true);
@ -64,7 +64,7 @@ class Mediawiki
/** /**
* Checks for each person whether they are alive according to Wikipedia's categorization. * Checks for each person whether they are alive according to Wikipedia's categorization.
* *
* @param array $people_names the names of the people to check aliveness of * @param array<string> $people_names the names of the people to check aliveness of
* @return Response responds positively with each person's aliveness, or a negative response explaining what went * @return Response responds positively with each person's aliveness, or a negative response explaining what went
* wrong * wrong
*/ */

View File

@ -18,7 +18,7 @@ class Response
public bool $satisfied; public bool $satisfied;
public function __construct($message, $satisfied) public function __construct(mixed $message, bool $satisfied)
{ {
$this->message = $message; $this->message = $message;
$this->satisfied = $satisfied; $this->satisfied = $satisfied;

View File

@ -57,6 +57,11 @@ class TrackingManager
{ {
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE); $db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE);
$stmt = $db->prepare("INSERT INTO trackings (user_uuid, person_name) VALUES (:user_uuid, :person_name);"); $stmt = $db->prepare("INSERT INTO trackings (user_uuid, person_name) VALUES (:user_uuid, :person_name);");
if ($stmt === false) {
$db->close();
$this->logger->error("Failed to prepare query in 'add_tracking'.");
return new Response("Unexpected database error.", false);
}
$stmt->bindValue(":user_uuid", $user_uuid); $stmt->bindValue(":user_uuid", $user_uuid);
$stmt->bindValue(":person_name", $person_name); $stmt->bindValue(":person_name", $person_name);
$inserted = $stmt->execute() !== false; $inserted = $stmt->execute() !== false;
@ -76,6 +81,11 @@ class TrackingManager
{ {
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE); $db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE);
$stmt = $db->prepare("DELETE FROM trackings WHERE user_uuid=:user_uuid AND person_name=:person_name;"); $stmt = $db->prepare("DELETE FROM trackings WHERE user_uuid=:user_uuid AND person_name=:person_name;");
if ($stmt === false) {
$db->close();
$this->logger->error("Failed to prepare query in 'remove_tracking'.");
return new Response("Unexpected database error.", false);
}
$stmt->bindValue(":user_uuid", $user_uuid); $stmt->bindValue(":user_uuid", $user_uuid);
$stmt->bindValue(":person_name", $person_name); $stmt->bindValue(":person_name", $person_name);
$inserted = $stmt->execute() !== false; $inserted = $stmt->execute() !== false;
@ -94,8 +104,18 @@ class TrackingManager
{ {
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READONLY); $db = new SQLite3($this->db_filename, SQLITE3_OPEN_READONLY);
$stmt = $db->prepare("SELECT * FROM trackings WHERE user_uuid=:user_uuid;"); $stmt = $db->prepare("SELECT * FROM trackings WHERE user_uuid=:user_uuid;");
if ($stmt === false) {
$db->close();
$this->logger->error("Failed to prepare query in 'list_trackings'.");
return new Response("Unexpected database error.", false);
}
$stmt->bindValue(":user_uuid", $user_uuid); $stmt->bindValue(":user_uuid", $user_uuid);
$results = $stmt->execute(); $results = $stmt->execute();
if ($results === false) {
$db->close();
$this->logger->error("Failed to read query results in 'list_trackings'.");
return new Response("Unexpected database error.", false);
}
$trackings = []; $trackings = [];
while ($row = $results->fetchArray(SQLITE3_ASSOC)) while ($row = $results->fetchArray(SQLITE3_ASSOC))

View File

@ -74,8 +74,20 @@ class UserManager
// Check if email address is already in use // Check if email address is already in use
$stmt = $db->prepare("SELECT 1 FROM users WHERE email=:email;"); $stmt = $db->prepare("SELECT 1 FROM users WHERE email=:email;");
if ($stmt === false) {
$db->exec("COMMIT;");
$db->close();
$this->logger->error("Failed to prepare query in 'register'.");
return new Response("Unexpected database error.", false);
}
$stmt->bindValue(":email", $email); $stmt->bindValue(":email", $email);
$email_available = $stmt->execute()->fetchArray(SQLITE3_ASSOC) === false; $result = $stmt->execute();
if ($result === false) {
$db->exec("COMMIT;");
$db->close();
return new Response("Unexpected database error.", false);
}
$email_available = $result->fetchArray(SQLITE3_ASSOC) === false;
if (!$email_available) { if (!$email_available) {
$db->exec("COMMIT;"); $db->exec("COMMIT;");
$db->close(); $db->close();
@ -93,6 +105,12 @@ class UserManager
// Register user // Register user
$stmt = $db->prepare("INSERT INTO users (uuid, email, password) VALUES (:uuid, :email, :password);"); $stmt = $db->prepare("INSERT INTO users (uuid, email, password) VALUES (:uuid, :email, :password);");
if ($stmt === false) {
$db->exec("COMMIT;");
$db->close();
$this->logger->error("Failed to prepare query in 'register'.");
return new Response("Unexpected database error.", false);
}
$stmt->bindValue(":uuid", $uuid); $stmt->bindValue(":uuid", $uuid);
$stmt->bindValue(":email", $email); $stmt->bindValue(":email", $email);
$stmt->bindValue(":password", password_hash($password, PASSWORD_DEFAULT)); $stmt->bindValue(":password", password_hash($password, PASSWORD_DEFAULT));
@ -101,7 +119,7 @@ class UserManager
if (!$user_registered) { if (!$user_registered) {
$db->exec("COMMIT;"); $db->exec("COMMIT;");
$db->close(); $db->close();
return new Response("Unknown database error.", false); return new Response("Unexpected database error.", false);
} }
// Respond // Respond
@ -122,6 +140,11 @@ class UserManager
{ {
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE); $db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE);
$stmt = $db->prepare("DELETE FROM users WHERE uuid=:uuid;"); $stmt = $db->prepare("DELETE FROM users WHERE uuid=:uuid;");
if ($stmt === false) {
$db->close();
$this->logger->error("Failed to prepare query in 'delete'.");
return new Response("Unexpected database error.", false);
}
$stmt->bindValue(":uuid", $uuid); $stmt->bindValue(":uuid", $uuid);
$result = $stmt->execute(); $result = $stmt->execute();
$db->close(); $db->close();
@ -136,16 +159,27 @@ class UserManager
* *
* @param string $email the email address of the user whose password should be checked * @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 * @param string $password the password to check against the specified user
* @return array the first element is a response with message `null` if the login was successful, or a response with * @return array{Response, ?string} the first element is a response with message `null` if the login was successful,
* a message explaining what went wrong otherwise; the second element is the UUID of the user that was logged in as, * or a response with a message explaining what went wrong otherwise; the second element is the UUID of the user
* or `null` if the login should not be performed * that was logged in as, or `null` if the login should not be performed
*/ */
public function check_login(string $email, string $password): array public function check_login(string $email, string $password): array
{ {
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READONLY); $db = new SQLite3($this->db_filename, SQLITE3_OPEN_READONLY);
$stmt = $db->prepare("SELECT uuid, password FROM users WHERE email=:email;"); $stmt = $db->prepare("SELECT uuid, password FROM users WHERE email=:email;");
if ($stmt === false) {
$db->close();
$this->logger->error("Failed to prepare query in 'check_login'.");
return [new Response("Unexpected database error.", false), null];
}
$stmt->bindValue(":email", $email); $stmt->bindValue(":email", $email);
$user = $stmt->execute()->fetchArray(SQLITE3_ASSOC); $result = $stmt->execute();
if ($result === false) {
$db->close();
$this->logger->error("Failed to execute query in 'check_login'.");
return [new Response("Unexpected database error.", false), null];
}
$user = $result->fetchArray(SQLITE3_ASSOC);
$db->close(); $db->close();
return $user === false || !password_verify($password, $user["password"]) return $user === false || !password_verify($password, $user["password"])
@ -163,8 +197,19 @@ class UserManager
{ {
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READONLY); $db = new SQLite3($this->db_filename, SQLITE3_OPEN_READONLY);
$stmt = $db->prepare("SELECT * FROM users WHERE uuid=:uuid;"); $stmt = $db->prepare("SELECT * FROM users WHERE uuid=:uuid;");
if ($stmt === false) {
$db->close();
$this->logger->error("Failed to prepare query in 'get_user_data'.");
return new Response("Unexpected database error.", false);
}
$stmt->bindValue(":uuid", $uuid); $stmt->bindValue(":uuid", $uuid);
$user = $stmt->execute()->fetchArray(SQLITE3_ASSOC); $result = $stmt->execute();
if ($result === false) {
$db->close();
$this->logger->error("Failed to execute query in 'get_user_data'.");
return new Response("Unexpected database error.", false);
}
$user = $result->fetchArray(SQLITE3_ASSOC);
$db->close(); $db->close();
return $user === false return $user === false
@ -192,8 +237,21 @@ class UserManager
// Check if email address is already in use // Check if email address is already in use
$stmt = $db->prepare("SELECT 1 FROM users WHERE email=:email;"); $stmt = $db->prepare("SELECT 1 FROM users WHERE email=:email;");
if ($stmt === false) {
$db->exec("COMMIT;");
$db->close();
$this->logger->error("Failed to prepare query in 'set_email'.");
return new Response("Unexpected database error.", false);
}
$stmt->bindValue(":email", $email); $stmt->bindValue(":email", $email);
$email_available = $stmt->execute()->fetchArray(SQLITE3_ASSOC) === false; $result = $stmt->execute();
if ($result === false) {
$db->exec("COMMIT;");
$db->close();
$this->logger->error("Failed to execute query in 'set_email'.");
return new Response("Email address already in use.", false);
}
$email_available = $result->fetchArray(SQLITE3_ASSOC) === false;
if (!$email_available) { if (!$email_available) {
$db->exec("COMMIT;"); $db->exec("COMMIT;");
$db->close(); $db->close();
@ -202,6 +260,12 @@ class UserManager
// Update email address // Update email address
$stmt = $db->prepare("UPDATE users SET email=:email WHERE uuid=:uuid;"); $stmt = $db->prepare("UPDATE users SET email=:email WHERE uuid=:uuid;");
if ($stmt === false) {
$db->exec("COMMIT;");
$db->close();
$this->logger->error("Failed to prepare query in 'set_email'.");
return new Response("Unexpected database error.", false);
}
$stmt->bindValue(":uuid", $uuid); $stmt->bindValue(":uuid", $uuid);
$stmt->bindValue(":email", $email); $stmt->bindValue(":email", $email);
$email_updated = $stmt->execute() !== false; $email_updated = $stmt->execute() !== false;
@ -248,8 +312,21 @@ class UserManager
// Validate old password // Validate old password
$stmt = $db->prepare("SELECT * FROM users WHERE uuid=:uuid;"); $stmt = $db->prepare("SELECT * FROM users WHERE uuid=:uuid;");
if ($stmt === false) {
$db->exec("COMMIT;");
$db->close();
$this->logger->error("Failed to prepare query in 'set_password'.");
return new Response("Unexpected database error.", false);
}
$stmt->bindValue(":uuid", $uuid); $stmt->bindValue(":uuid", $uuid);
$user = $stmt->execute()->fetchArray(SQLITE3_ASSOC); $result = $stmt->execute();
if ($result === false) {
$db->exec("COMMIT;");
$db->close();
$this->logger->error("Failed to execute query in 'set_password'.");
return new Response("Invalid user.", false);
}
$user = $result->fetchArray(SQLITE3_ASSOC);
if ($user === false) { if ($user === false) {
$db->exec("COMMIT;"); $db->exec("COMMIT;");
$db->close(); $db->close();
@ -263,6 +340,12 @@ class UserManager
// Update password // Update password
$stmt = $db->prepare("UPDATE users SET password=:password WHERE uuid=:uuid;"); $stmt = $db->prepare("UPDATE users SET password=:password WHERE uuid=:uuid;");
if ($stmt === false) {
$db->exec("COMMIT;");
$db->close();
$this->logger->error("Failed to prepare query in 'set_password'.");
return new Response("Unexpected database error.", false);
}
$stmt->bindValue(":uuid", $uuid); $stmt->bindValue(":uuid", $uuid);
$stmt->bindValue(":password", password_hash($password_new, PASSWORD_DEFAULT)); $stmt->bindValue(":password", password_hash($password_new, PASSWORD_DEFAULT));
$password_updated = $stmt->execute() !== false; $password_updated = $stmt->execute() !== false;