\n"; include __DIR__ . "/application/views/" . $___NAME . ".php"; echo "\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, "> ")) { if (!$inQuote) { $contents .= implode("\n", $lineBuf); $lineBuf = []; $inQuote = true; } $lineBuf []= substr($ln, 5); } else { if ($inQuote) { $contents .= "
\n" . implode("\n", $lineBuf) . "
\n"; $lineBuf = []; $inQuote = false; if (trim($ln) === "
") continue; } $lineBuf []= $ln; } } if ($inQuote) { $contents .= "
" . implode("\n", $lineBuf) . "
"; } else { $contents .= implode("\n", $lineBuf); } return $contents; } require_once __DIR__ . "/vendor/autoload.php"; $db = null; try { $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!"), Messaging::italic($ex->getMessage()), ]); 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"; 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); Messaging::info([ Messaging::bold("Superuser account created"), [ "Username" => $superuser->name, "Password" => $superUserPassword, ], "Please note that the password can only be shown this time, so please note it down!", ]); 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('

Please click here to log in!

'), ]); } 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"); 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) { _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"); }