summaryrefslogtreecommitdiff
path: root/src/index.php
diff options
context:
space:
mode:
authorJonas Kohl <git@jonaskohl.de>2024-09-12 19:49:17 +0200
committerJonas Kohl <git@jonaskohl.de>2024-09-12 19:49:17 +0200
commit086e2d2668784469ec114f6e6fd2b3dace3d7c3b (patch)
treeb9bacedb713501d88d24085940267a7c94e69b29 /src/index.php
parent34b1b391d4b03659a96f868857c230002b351514 (diff)
Way more progress on forum
Diffstat (limited to 'src/index.php')
-rw-r--r--src/index.php509
1 files changed, 506 insertions, 3 deletions
diff --git a/src/index.php b/src/index.php
index 8e2ce4e..109cd2e 100644
--- a/src/index.php
+++ b/src/index.php
@@ -3,27 +3,99 @@
use mystic\forum\Database;
use mystic\forum\exceptions\DatabaseConnectionException;
use mystic\forum\Messaging;
+use mystic\forum\orm\Attachment;
use mystic\forum\orm\Post;
use mystic\forum\orm\Topic;
use mystic\forum\orm\User;
+use mystic\forum\orm\UserPermissions;
use mystic\forum\utils\RequestUtils;
+use mystic\forum\utils\ValidationUtils;
+
+header_remove("X-Powered-By");
function exception_error_handler($errno, $errstr, $errfile, $errline ) {
throw new ErrorException(html_entity_decode($errstr), $errno, 0, $errfile, $errline);
}
set_error_handler("exception_error_handler");
+define("REGISTRATION_ENABLED", true);
+
session_name("fsid");
session_start();
+const MAX_ATTACHMENT_SIZE = 0x200000;
+const MAX_ATTACHMENT_COUNT = 4;
+const THUMB_MAX_DIM = 100;
+
$_rq_method = $_SERVER["REQUEST_METHOD"] ?? "GET";
$_action = $_GET["_action"] ?? null;
+$GLOBALS["action"] = $_action;
+
+function _view(string $name, array $params = []): void {
+ $___NAME = $name;
+ extract($params);
+ echo "<!--{" . htmlentities($name) . "}-->\n";
+ include __DIR__ . "/application/views/" . $___NAME . ".php";
+ echo "<!--{/" . htmlentities($name) . "}-->\n";
+}
+
+function reArrayFiles(&$file_post) {
+ $file_ary = [];
+ $file_count = count($file_post['name']);
+ $file_keys = array_keys($file_post);
+
+ for ($i=0; $i<$file_count; $i++) {
+ if ($file_post["error"][$i] === UPLOAD_ERR_NO_FILE)
+ continue;
+ foreach ($file_keys as $key) {
+ $file_ary[$i][$key] = $file_post[$key][$i];
+ }
+ }
+
+ return $file_ary;
+}
+
+function renderPost(string $contents): string {
+ $contents = htmlentities($contents);
+ $contents = nl2br($contents, false);
+ $lines = explode("\n", $contents);
+ $contents = "";
+ $lineBuf = [];
+ $inQuote = false;
+ foreach ($lines as $ln) {
+ if (str_starts_with($ln, "&gt; ")) {
+ if (!$inQuote) {
+ $contents .= implode("\n", $lineBuf);
+ $lineBuf = [];
+ $inQuote = true;
+ }
+ $lineBuf []= substr($ln, 5);
+ } else {
+ if ($inQuote) {
+ $contents .= "<blockquote>\n" . implode("\n", $lineBuf) . "</blockquote>\n";
+ $lineBuf = [];
+ $inQuote = false;
+ if (trim($ln) === "<br>")
+ continue;
+ }
+ $lineBuf []= $ln;
+ }
+ }
+ if ($inQuote) {
+ $contents .= "<blockquote>" . implode("\n", $lineBuf) . "</blockquote>";
+ } else {
+ $contents .= implode("\n", $lineBuf);
+ }
+
+ return $contents;
+}
+
require_once __DIR__ . "/vendor/autoload.php";
$db = null;
try {
- $db = new Database(Database::getConnectionString("db", "postgres", "postgres", "postgres"));
+ $db = new Database(Database::getConnectionString("db", getenv("POSTGRES_USER"), getenv("POSTGRES_PASSWORD"), getenv("POSTGRES_DBNAME")));
} catch (DatabaseConnectionException $ex) {
Messaging::error([
Messaging::bold("Failed to connect to database!"),
@@ -32,9 +104,12 @@ try {
exit;
}
+$GLOBALS["db"] = &$db;
+
$db->ensureTable(User::class);
$db->ensureTable(Topic::class);
$db->ensureTable(Post::class);
+$db->ensureTable(Attachment::class);
$superuser = new User();
$superuser->id = "SUPERUSER";
@@ -42,9 +117,14 @@ if (!$db->fetch($superuser)) {
$superUserPassword = base64_encode(random_bytes(12));
$superuser->name = "superuser";
+ $superuser->email = "";
$superuser->passwordHash = password_hash($superUserPassword, PASSWORD_DEFAULT);
$superuser->displayName = "SuperUser";
$superuser->created = new \DateTimeImmutable();
+ $superuser->permissionMask = PHP_INT_MAX;
+ $superuser->passwordResetRequired = false;
+ $superuser->activated = true;
+ $superuser->activationToken = "";
$db->insert($superuser);
@@ -59,13 +139,436 @@ if (!$db->fetch($superuser)) {
exit;
}
+$currentUser = RequestUtils::getAuthorizedUser($db);
+$GLOBALS["currentUser"] = &$currentUser;
+
// initialization finished
if ($_action === "auth") {
+ if ($currentUser) {
+ header("Location: .");
+ exit;
+ }
+
+ if (RequestUtils::isRequestMethod("POST")) {
+ $username = RequestUtils::getRequiredField("username");
+ $password = RequestUtils::getRequiredField("password");
+
+ $user = new User();
+ $user->name = $username;
+ if (!$db->fetchWhere($user, "name") || !password_verify($password, $user->passwordHash)) {
+ RequestUtils::triggerFormError("Username or password incorrect!");
+ }
+
+ if (!$user->activated) {
+ RequestUtils::triggerFormError("Please activate your user account first!");
+ }
+
+ RequestUtils::setAuthorizedUser($user);
+ header("Location: .");
+ } else {
+ _view("template_start", ["_title" => "Forum"]);
+ _view("template_navigation_start");
+ _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
+ _view("template_navigation_end");
+ _view("form_login");
+ _view("template_end");
+ }
+} elseif ($_action === "register") {
+ if ($currentUser) {
+ header("Location: .");
+ exit;
+ }
+
+ if (!REGISTRATION_ENABLED) {
+ http_response_code(403);
+ Messaging::error("Public registration disabled");
+ exit;
+ }
+
+ if (RequestUtils::isRequestMethod("POST")) {
+ $username = RequestUtils::getRequiredField("username");
+ $password = RequestUtils::getRequiredField("password");
+ $passwordRetype = RequestUtils::getRequiredField("password_retype");
+ $email = trim(RequestUtils::getRequiredField("email"));
+ $displayName = RequestUtils::getRequiredField("display_name");
+
+ // usernames are always lowercase
+ $username = strtolower($username);
+
+ if ($password !== $passwordRetype) {
+ RequestUtils::triggerFormError("Passwords do not match!");
+ }
+
+ if (strlen($password) < 8) {
+ RequestUtils::triggerFormError("Password too short! Your password must consist of 8 or more characters");
+ }
+
+ if (!ValidationUtils::isUsernameValid($username)) {
+ RequestUtils::triggerFormError("Username has an invalid format");
+ }
+
+ if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
+ RequestUtils::triggerFormError("Invalid email address");
+ }
+
+ $user = new User();
+ $user->name = $username;
+ $user->email = $email;
+
+ if ($db->fetchWhere($user, "name")) {
+ RequestUtils::triggerFormError("This username is already taken!");
+ }
+
+ if ($db->fetchWhere($user, "email")) {
+ RequestUtils::triggerFormError("This email address is already in use!");
+ }
+
+ // re-create user so we don't forget to clear properties set by the above queries
+
+ $user = new User();
+ $user->id = $db->generateId();
+ $user->displayName = $displayName;
+ $user->name = $username;
+ $user->email = $email;
+ $user->passwordHash = password_hash($password, PASSWORD_DEFAULT);
+ $user->permissionMask = UserPermissions::GROUP_USER;
+ $user->passwordResetRequired = false;
+ $user->activated = false;
+ $user->activationToken = $db->generateId(12);
+ $user->created = new \DateTimeImmutable();
+
+ // TODO Send verification email
+
+ $db->insert($user);
+
+ Messaging::info([
+ "Your account has been created!",
+ //"Please check your emails for an activation link!",
+ Messaging::html('<p>Please click <a href="?_action=auth">here</a> to log in!</p>'),
+ ]);
+ } else {
+ _view("template_start", ["_title" => "Forum"]);
+ _view("template_navigation_start");
+ _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
+ _view("template_navigation_end");
+ _view("form_register");
+ _view("template_end");
+ }
+} elseif ($_action === "logout") {
+ RequestUtils::unsetAuthorizedUser();
+ header("Location: .");
+} elseif ($_action === "viewtopic") {
+ $topicId = $_GET["topic"] ?? throw new Exception("Missing topic id");
+ $topic = new Topic();
+ $topic->id = $topicId;
+ if (!$db->fetch($topic)) {
+ http_response_code(404);
+ Messaging::error("No topic exists with this id");
+ exit;
+ }
+
+ if (RequestUtils::isRequestMethod("POST")) {
+ if (!$currentUser) {
+ http_response_code(403);
+ Messaging::error("You need to be logged in to add new posts!");
+ exit;
+ }
+
+ $attachments = reArrayFiles($_FILES["files"]);
+
+ if (count($attachments) > MAX_ATTACHMENT_COUNT)
+ RequestUtils::triggerFormError("Too many attachments");
+
+ // check all attachments before saving one
+ foreach ($attachments as $att) {
+ if ($att["size"] > MAX_ATTACHMENT_SIZE) {
+ RequestUtils::triggerFormError("Individual file size exceeded");
+ }
+ }
+
+ $message = trim(RequestUtils::getRequiredField("message"));
+
+ if (strlen($message) < 1 || strlen($message) > 0x8000) {
+ RequestUtils::triggerFormError("Message too short or too long!");
+ }
+
+ $post = new Post();
+ $post->id = $db->generateId();
+ $post->authorId = $currentUser->id;
+ $post->topicId = $topicId;
+ $post->content = $message;
+ $post->postDate = new DateTimeImmutable();
+ $post->deleted = false;
+
+ $db->insert($post);
+
+ foreach ($attachments as $att) {
+ [
+ "name" => $name,
+ "type" => $type,
+ "tmp_name" => $tmpName,
+ ] = $att;
+ $attachment = new Attachment();
+ $attachment->id = $db->generateId();
+ $attachment->name = $name;
+ $attachment->mimeType = $type;
+ $attachment->postId = $post->id;
+ $attachment->contents = file_get_contents($tmpName);
+
+ $db->insert($attachment);
+ }
+
+ header("Location: ?_action=viewtopic&topic=" . urlencode($topicId) . "#form");
+ } else {
+ $posts = $db->fetchCustom(Post::class, 'WHERE topic_id = $1 ORDER BY post_date', [ $topicId ]);
+ $userCache = [];
+
+ $topicAuthor = null;
+ if ($topic->createdBy !== null) {
+ $topicAuthor = new User();
+ $topicAuthor->id = $topic->createdBy;
+ if (!$db->fetch($topicAuthor)) {
+ $topicAuthor = null;
+ }
+ }
+
+ _view("template_start", ["_title" => "Forum"]);
+ _view("template_navigation_start");
+ _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
+ _view("template_navigation_end");
+ _view("view_topic_start", ["topic" => $topic, "topicAuthor" => $topicAuthor]);
+
+ /** @var Post $post */
+ foreach ($posts as $post) {
+ /** @var ?User $postAuthor */
+ $postAuthor = null;
+ if ($post->authorId !== null && !isset($userCache[$post->authorId])) {
+ $usr = new User();
+ $usr->id = $post->authorId;
+ if ($db->fetch($usr))
+ $userCache[$post->authorId] = &$usr;
+ }
+ if (isset($userCache[$post->authorId]))
+ $postAuthor = &$userCache[$post->authorId];
+
+ $attachments = $db->fetchCustom(Attachment::class, 'WHERE post_id = $1', [ $post->id ]);
+
+ _view("view_post", [
+ "post" => $post,
+ "postAuthor" => $postAuthor,
+ "attachments" => $attachments,
+ ]);
+ }
+
+ _view("view_topic_end");
+
+ if ($currentUser) {
+ _view("form_addpost");
+ }
+
+ _view("template_end");
+ }
+
+} elseif ($_action === "newtopic") {
+ if (!$currentUser) {
+ http_response_code(403);
+ Messaging::error("You need to be logged in to create new topics!");
+ exit;
+ }
+
+ if (RequestUtils::isRequestMethod("POST")) {
+ $title = trim(RequestUtils::getRequiredField("title"));
+ $message = trim(RequestUtils::getRequiredField("message"));
+
+ if (strlen($title) < 1 || strlen($title) > 255) {
+ RequestUtils::triggerFormError("Title too short or too long!");
+ }
+
+ if (strlen($message) < 1 || strlen($message) > 0x8000) {
+ RequestUtils::triggerFormError("Message too short or too long!");
+ }
+
+ $topic = new Topic();
+ $topic->createdBy = $currentUser->id;
+ $topic->id = $db->generateId();
+ $topic->title = $title;
+ $topic->creationDate = new DateTimeImmutable();
+
+ $db->insert($topic);
+
+ $post = new Post();
+ $post->id = $db->generateId();
+ $post->authorId = $currentUser->id;
+ $post->topicId = $topic->id;
+ $post->content = $message;
+ $post->postDate = $topic->creationDate;
+ $post->deleted = false;
+
+ $db->insert($post);
+
+ header("Location: ?_action=viewtopic&topic=" . urlencode($topic->id));
+ } else {
+ _view("template_start", ["_title" => "Forum"]);
+ _view("template_navigation_start");
+ _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
+ _view("template_navigation_end");
+ _view("form_newtopic");
+ _view("template_end");
+ }
+} elseif ($_action === "viewuser") {
+ $userId = $_GET["user"] ?? throw new Exception("Missing user id");
+ $user = new User();
+ $user->id = $userId;
+ if (!$db->fetch($user)) {
+ http_response_code(404);
+ Messaging::error("No user exists with this id");
+ exit;
+ }
+ _view("template_start", ["_title" => "Forum"]);
+ _view("template_navigation_start");
+ _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
+ _view("template_navigation_end");
+ _view("view_user", ["user" => $user]);
+ _view("template_end");
+} elseif ($_action === "attachment") {
+ if (!$currentUser) {
+ http_response_code(403);
+ Messaging::error("You must be logged in to view attachments");
+ exit;
+ }
+
+ $attId = $_GET["attachment"] ?? throw new Exception("Missing attachment id");
+ $attachment = new Attachment();
+ $attachment->id = $attId;
+ if (!$db->fetch($attachment)) {
+ http_response_code(404);
+ Messaging::error("No attachment exists with this id");
+ exit;
+ }
+
+ header("Content-Type: " . $attachment->mimeType);
+ header("Content-Length: " . strlen($attachment->contents));
+ header("Cache-Control: no-cache");
+ echo $attachment->contents;
+} elseif ($_action === "thumb") {
+
+ $attId = $_GET["attachment"] ?? throw new Exception("Missing attachment id");
+ $attachment = new Attachment();
+ $attachment->id = $attId;
+ if (!$db->fetch($attachment)) {
+ http_response_code(404);
+ Messaging::error("No attachment exists with this id");
+ exit;
+ }
+
+ if (!str_starts_with($attachment->mimeType, "image/")) {
+ http_response_code(400);
+ Messaging::error("Attachment is not an image");
+ exit;
+ }
+
+ // TODO Cache thumbnail
+ $im = imagecreatefromstring($attachment->contents);
+ $w = imagesx($im);
+ $h = imagesy($im);
+ $r = $w / floatval($h);
+
+ if ($w > $h) {
+ $nw = THUMB_MAX_DIM;
+ $nh = floor($nw / $r);
+ } else {
+ $nh = THUMB_MAX_DIM;
+ $nw = floor($r * $nh);
+ }
+
+ $thumb = imagecreatetruecolor($nw, $nh);
+ imagecopyresampled($thumb, $im, 0, 0, 0, 0, $nw, $nh, $w, $h);
+ imagedestroy($im);
+
+ header("Content-Type: image/jpeg");
+ header("Cache-Control: no-cache");
+ imagejpeg($thumb, null, 40);
+ imagedestroy($thumb);
+} elseif ($_action === "deletepost") {
RequestUtils::ensureRequestMethod("POST");
- // TODO Login logic
+
+ if (!$currentUser) {
+ http_response_code(403);
+ Messaging::error("You need to be logged in to delete posts!");
+ exit;
+ }
+
+ $postId = RequestUtils::getRequiredField("post");
+
+ $post = new Post();
+ $post->id = $postId;
+
+ if (!$db->fetch($post)) {
+ http_response_code(404);
+ Messaging::error("No post exists with this id");
+ exit;
+ }
+
+ $postAuthor = new User();
+ $postAuthor->id = $post->authorId;
+
+ if (!$db->fetch($postAuthor))
+ $postAuthor = null;
+
+ $canDelete = ($currentUser->id === $postAuthor?->id && $postAuthor?->hasPermission(UserPermissions::DELETE_OWN_POST))
+ || ($currentUser->hasPermission(UserPermissions::DELETE_OTHER_POST));
+
+ if (!$canDelete) {
+ http_response_code(403);
+ Messaging::error("You don't have permission to delete this post");
+ exit;
+ }
+
+ $confirm = $_POST["confirm"] ?? null;
+ if ($confirm !== null) {
+ $expectedConfirm = base64_encode(hash("sha256", "confirm" . $post->id, true));
+ if ($confirm !== $expectedConfirm) {
+ http_response_code(400);
+ Messaging::error("Invalid confirmation");
+ exit;
+ }
+
+ $post->deleted = true;
+ $post->content = "";
+
+ if (!$db->update($post)) {
+ http_response_code(500);
+ Messaging::error("Failed to delete post");
+ exit;
+ }
+
+ $attachments = $db->fetchCustom(Attachment::class, 'WHERE post_id = $1', [ $post->id ]);
+
+ foreach ($attachments as $attachment) {
+ if (!$db->delete($attachment)) {
+ http_response_code(500);
+ Messaging::error("Failed to delete attachment");
+ exit;
+ }
+ }
+
+ header("Location: ?_action=viewtopic&topic=" . urlencode($post->topicId));
+ } else {
+ _view("template_start", ["_title" => "Forum"]);
+ _view("template_navigation_start");
+ _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
+ _view("template_navigation_end");
+ _view("form_delete_post_confirm", ["post" => $post]);
+ _view("template_end");
+ }
} elseif ($_action === null) {
- echo "Hello";
+ _view("template_start", ["_title" => "Forum"]);
+ _view("template_navigation_start");
+ _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
+ _view("template_navigation_end");
+ _view("view_topics", ["topics" => $db->fetchCustom(Topic::class, "ORDER BY creation_date DESC")]);
+ _view("template_end");
} else {
http_response_code(404);
Messaging::error("Invalid or unknown action $_action");