diff --git a/composer.json b/composer.json index 17bbfd0..14a15f4 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.4", + "version": "0.0.5", "type": "project", "license": "MIT", "homepage": "https://git.fwdekker.com/tools/death-notifier", @@ -18,8 +18,11 @@ "require": { "ext-curl": "*", "ext-sqlite3": "*", - "monolog/monolog": "^3.2.0", - "phpmailer/phpmailer": "^6.6.3" + "monolog/monolog": "^3.2", + "phpmailer/phpmailer": "^6.6" + }, + "require-dev": { + "phpstan/phpstan": "^1.8" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index a7a35f8..b24808d 100644 Binary files a/composer.lock and b/composer.lock differ diff --git a/package.json b/package.json index 0f608a1..f997e32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "death-notifier", - "version": "0.0.4", + "version": "0.0.5", "description": "Get notified when a famous person dies.", "author": "Florine W. Dekker", "browser": "dist/bundle.js", @@ -13,7 +13,8 @@ "clean": "grunt clean", "dev": "grunt dev", "dev:server": "grunt dev:server", - "deploy": "grunt deploy" + "deploy": "grunt deploy", + "stan": "vendor/bin/phpstan analyse -l 8 src/main" }, "devDependencies": { "grunt": "^1.5.3", diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..08de7f8 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + excludePaths: + - src/main/config.default.ini.php + - src/main/config.ini.php diff --git a/src/main/api.php b/src/main/api.php index d6fc3c9..47519fe 100644 --- a/src/main/api.php +++ b/src/main/api.php @@ -15,9 +15,10 @@ require_once __DIR__ . "/vendor/autoload.php"; /// Preparations // 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")) { - $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); } @@ -50,12 +51,12 @@ if (!file_exists($config["database"]["filename"])) { * * @return string the generated CSRF token */ -function generate_csrf_token(): string +function generate_csrf_token(Logger $logger): string { try { return bin2hex(random_bytes(32)); } catch (Exception $exception) { - $log->emergency("Failed to generate token.", [$exception]); + $logger->emergency("Failed to generate token.", [$exception]); http_response_code(500); exit(); } @@ -63,10 +64,11 @@ function generate_csrf_token(): string session_start(); if (!isset($_SESSION["token"])) - $_SESSION["token"] = generate_csrf_token(); + $_SESSION["token"] = generate_csrf_token($logger); // 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 @@ -153,7 +155,7 @@ if (isset($_POST["action"])) { session_destroy(); session_start(); - $_SESSION["token"] = generate_csrf_token(); + $_SESSION["token"] = generate_csrf_token($logger); $response = new Response(null, true); break; case "update-email": @@ -194,7 +196,7 @@ if (isset($_POST["action"])) { $response = validate_csrf() ?? $mailer->send_test_mail(); break; default: - $response["message"] = "Unknown POST action '" . $_POST["action"] . "'."; + $response = new Response("Unknown POST action '" . $_POST["action"] . "'.", false); break; } } else if (isset($_GET["action"])) { diff --git a/src/main/php/Mediawiki.php b/src/main/php/Mediawiki.php index 808d9a9..bed7994 100644 --- a/src/main/php/Mediawiki.php +++ b/src/main/php/Mediawiki.php @@ -42,11 +42,11 @@ class Mediawiki /** * 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 - * @return array a JSON object containing the API's response + * @param array $url_param the query parameters to send to the API + * @return mixed a JSON object containing the API's response * @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(); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); @@ -55,7 +55,7 @@ class Mediawiki $output = curl_exec($ch); curl_close($ch); - if (curl_error($ch)) + if (is_bool($output) || curl_error($ch)) throw new Exception(curl_error($ch)); 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. * - * @param array $people_names the names of the people to check aliveness of + * @param array $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 * wrong */ diff --git a/src/main/php/Response.php b/src/main/php/Response.php index 8ef371c..d50bb8c 100644 --- a/src/main/php/Response.php +++ b/src/main/php/Response.php @@ -18,7 +18,7 @@ class Response public bool $satisfied; - public function __construct($message, $satisfied) + public function __construct(mixed $message, bool $satisfied) { $this->message = $message; $this->satisfied = $satisfied; diff --git a/src/main/php/TrackingManager.php b/src/main/php/TrackingManager.php index 50b26f4..785028a 100644 --- a/src/main/php/TrackingManager.php +++ b/src/main/php/TrackingManager.php @@ -57,6 +57,11 @@ class TrackingManager { $db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE); $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(":person_name", $person_name); $inserted = $stmt->execute() !== false; @@ -76,6 +81,11 @@ class TrackingManager { $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;"); + 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(":person_name", $person_name); $inserted = $stmt->execute() !== false; @@ -94,8 +104,18 @@ class TrackingManager { $db = new SQLite3($this->db_filename, SQLITE3_OPEN_READONLY); $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); $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 = []; while ($row = $results->fetchArray(SQLITE3_ASSOC)) diff --git a/src/main/php/UserManager.php b/src/main/php/UserManager.php index b520ae3..36a4742 100644 --- a/src/main/php/UserManager.php +++ b/src/main/php/UserManager.php @@ -74,8 +74,20 @@ class UserManager // Check if email address is already in use $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); - $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) { $db->exec("COMMIT;"); $db->close(); @@ -93,6 +105,12 @@ class UserManager // Register user $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(":email", $email); $stmt->bindValue(":password", password_hash($password, PASSWORD_DEFAULT)); @@ -101,7 +119,7 @@ class UserManager if (!$user_registered) { $db->exec("COMMIT;"); $db->close(); - return new Response("Unknown database error.", false); + return new Response("Unexpected database error.", false); } // Respond @@ -122,6 +140,11 @@ class UserManager { $db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE); $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); $result = $stmt->execute(); $db->close(); @@ -136,16 +159,27 @@ 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 the first element is a response with message `null` if the login was successful, or a response with - * a message explaining what went wrong otherwise; the second element is the UUID of the user that was logged in as, - * or `null` if the login should not be performed + * @return array{Response, ?string} the first element is a response with message `null` if the login was successful, + * or a response with a message explaining what went wrong otherwise; the second element is the UUID of the user + * that was logged in as, or `null` if the login should not be performed */ public function check_login(string $email, string $password): array { $db = new SQLite3($this->db_filename, SQLITE3_OPEN_READONLY); $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); - $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(); return $user === false || !password_verify($password, $user["password"]) @@ -163,8 +197,19 @@ class UserManager { $db = new SQLite3($this->db_filename, SQLITE3_OPEN_READONLY); $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); - $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(); return $user === false @@ -192,8 +237,21 @@ class UserManager // Check if email address is already in use $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); - $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) { $db->exec("COMMIT;"); $db->close(); @@ -202,6 +260,12 @@ class UserManager // Update email address $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(":email", $email); $email_updated = $stmt->execute() !== false; @@ -248,8 +312,21 @@ class UserManager // Validate old password $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); - $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) { $db->exec("COMMIT;"); $db->close(); @@ -263,6 +340,12 @@ class UserManager // Update password $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(":password", password_hash($password_new, PASSWORD_DEFAULT)); $password_updated = $stmt->execute() !== false;