\n"; include __DIR__ . "/application/views/" . $___NAME . ".php"; echo "\n"; } function isTrue(string $str): bool { $str = strtolower($str); return in_array($str, ["yes","true","y","t","on","enabled","1","?1"]); } 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_OK) 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; } function env(string $key): ?string { $val = getenv($key); if ($val === false) return null; return $val; } require_once __DIR__ . "/vendor/autoload.php"; require_once __DIR__ . "/application/i18n.php"; i18n_locale("de"); $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: " . ($_GET["next"] ?? ".")); 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: " . ($_GET["next"] ?? ".")); } else { _view("template_start", ["_title" => __("Log in")]); _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: " . ($_GET["next"] ?? ".")); exit; } if (!REGISTRATION_ENABLED) { http_response_code(403); Messaging::error(__("Public registration disabled")); exit; } if (RequestUtils::isRequestMethod("POST")) { $doNotFill = $_POST["username"] ?? null; if (!empty($doNotFill)) { sleep(10); http_response_code(204); exit; } $username = RequestUtils::getRequiredField("df82a9bc21"); $password = RequestUtils::getRequiredField("password"); $passwordRetype = RequestUtils::getRequiredField("password_retype"); $email = trim(RequestUtils::getRequiredField("email")); $displayName = RequestUtils::getRequiredField("display_name"); $captcha = RequestUtils::getRequiredField("captcha"); if ($captcha !== ($_SESSION["captchaPhrase"] ?? null)) { RequestUtils::triggerFormError(__("Incorrect CAPTCHA text!")); } // 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(); Transport::fromDsn(env("MAILER_DSN"))->send( (new Email()) ->from(env("MAILER_FROM")) ->to(new Address($email, $displayName)) ->text(__( "Welcome to %forum_title%, %user_display_name%!\n" . "\n" . "Please activate your account by clicking the link below:\n" . "%activation_link%\n" . "\n" . "Kind regards,\n" . "%forum_copyright%", params: [ "forum_title" => (env("MYSTIC_FORUM_TITLE") ?? "Forum"), "user_display_name" => $displayName, "activation_link" => env("PUBLIC_URL") . "?_action=verifyemail&token=" . urlencode($user->activationToken) . "&sig=" . urlencode(base64_encode(hash("sha256", env("SECRET") . $user->activationToken . $user->id, true))), "forum_copyright" => (env("MYSTIC_FORUM_COPYRIGHT") ?? env("MYSTIC_FORUM_TITLE") ?? "Forum") ] )) ->subject("Please activate your account") ); $db->insert($user); Messaging::info( Messaging::html(nl2br(htmlentities(__("Your account has been created!\nPlease check your emails for an activation link!"), true))) ); } else { _view("template_start", ["_title" => __("Register")]); _view("template_navigation_start"); _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]); _view("template_navigation_end"); _view("form_register"); _view("template_end"); } } elseif ($_action === "verifyemail") { RequestUtils::ensureRequestMethod("GET"); $token = $_GET["token"] ?? throw new Exception("Missing token"); $sig = $_GET["sig"] ?? throw new Exception("Missing signature"); $user = new User(); $user->activated = false; $user->activationToken = $token; if (!$db->fetchWhere($user, [ "activated", "activation_token" ])) { http_response_code(400); Messaging::error(__("Invalid token")); exit; } $expectedSignature = base64_encode(hash("sha256", env("SECRET") . $user->activationToken . $user->id, true)); if ($expectedSignature !== $sig) { http_response_code(400); Messaging::error(__("Invalid signature.")); exit; } $user->activated = true; $user->activationToken = ""; if (!$db->update($user)) { http_response_code(400); Messaging::error(__("Failed to update user")); exit; } Messaging::info([ Messaging::html(nl2br(__( "Your account has been activated!\nPlease click %link%here%/link% to log in!", [ "link" => '', "/link" => '', ] )), true), ]); } elseif ($_action === "logout") { RequestUtils::unsetAuthorizedUser(); header("Location: " . ($_GET["next"] ?? ".")); } 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; $post->edited = 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" => $topic->title]); _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 $topicAuthor */ $topicAuthor = 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])) $topicAuthor = &$userCache[$post->authorId]; $attachments = $db->fetchCustom(Attachment::class, 'WHERE post_id = $1', [ $post->id ]); _view("view_post", [ "post" => $post, "postAuthor" => $topicAuthor, "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")); $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")); } } 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; $post->edited = 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($topic->id)); } else { _view("template_start", ["_title" => __("New topic")]); _view("template_navigation_start"); _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]); _view("template_navigation_end"); _view("form_newtopic"); _view("template_end"); } } elseif ($_action === "lookupuser") { RequestUtils::ensureRequestMethod("GET"); $userHandle = $_GET["handle"] ?? throw new Exception("Missing handle"); $user = new User(); $user->name = $userHandle; if (!$db->fetchWhere($user, "name")) { http_response_code(404); Messaging::error(__("No user with name @%user_handle%", [ "user_handle" => $userHandle ])); exit; } header("Location: ./?_action=viewuser&user=" . urlencode($user->id)); } 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; } $lastNameChangeTooRecent = false; $isOwnProfile = $user->id === $currentUser?->id; if ($isOwnProfile && $user->nameLastChanged !== null) { $diff = $user->nameLastChanged->diff(new DateTime()); $diffSeconds = (new DateTime())->setTimestamp(0)->add($diff)->getTimestamp(); $diffDays = $diffSeconds / 60.0 / 60.0 / 24.0 / 30.0; $lastNameChangeTooRecent = $diffDays <= 30; } if (RequestUtils::isRequestMethod("POST")) { $displayName = RequestUtils::getRequiredField("display_name"); $pfpAction = RequestUtils::getRequiredField("pfp_action"); $userName = $_POST["name"] ?? $user->name; $user->displayName = $displayName; $userName = strtolower($userName); if ($userName !== $user->name) { if ($lastNameChangeTooRecent) { RequestUtils::triggerFormError(__("You can only change your username every 30 days!")); } else { if (!ValidationUtils::isUsernameValid($userName)) RequestUtils::triggerFormError(__("Invalid username!")); if (!ValidationUtils::isUsernameAvailable($db, $userName)) RequestUtils::triggerFormError(__("This username is already taken!")); $user->name = $userName; $user->nameLastChanged = new DateTimeImmutable(); } } switch ($pfpAction) { case "keep": // Do nothing break; case "remove": $user->profilePicture = null; break; case "replace": { if (!isset($_FILES["pfp"]) || $_FILES["pfp"]["error"] !== UPLOAD_ERR_OK) { RequestUtils::triggerFormError(__("Please upload an image to change your profile picture")); } $im = @imagecreatefromjpeg($_FILES["pfp"]["tmp_name"]); if ($im === false) $im = @imagecreatefrompng($_FILES["pfp"]["tmp_name"]); if ($im === false) RequestUtils::triggerFormError(__("Please upload a valid PNG or JPEG file")); /** @var \GdImage $im */ $thumb = imagecreatetruecolor(64, 64); imagecopyresampled($thumb, $im, 0, 0, 0, 0, 64, 64, imagesx($im), imagesy($im)); imagedestroy($im); $stream = fopen("php://memory", "w+"); imagejpeg($thumb, $stream, 50); rewind($stream); imagedestroy($thumb); $user->profilePicture = stream_get_contents($stream); fclose($stream); } break; default: RequestUtils::triggerFormError("Invalid value for pfp_action"); break; } if (!$db->update($user)) RequestUtils::triggerFormError(__("Failed to save changes", context: "Update profile")); header("Location: $_SERVER[REQUEST_URI]"); } else { $posts = $db->fetchCustom(Post::class, 'WHERE author_id = $1 ORDER BY post_date DESC', [ $userId ]); $topics = []; foreach ($posts as $post) { if (isset($topics[$post->topicId])) continue; $topic = new Topic(); $topic->id = $post->topicId; if (!$db->fetch($topic)) continue; $topics[$post->topicId] = $topic; } _view("template_start", ["_title" => $user->displayName]); _view("template_navigation_start"); _view("template_navigation", ["user" => $currentUser]); _view("template_navigation_end"); _view("view_user", [ "user" => $user, "posts" => $posts, "topics" => $topics, "lastNameChangeTooRecent" => $lastNameChangeTooRecent, ]); _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; } $name = preg_replace('/[\r\n\t\/]/', '_', $attachment->name); $extension = pathinfo($attachment->name, PATHINFO_EXTENSION); $mime = FileUtils::getMimeTypeForExtension($extension); switch ($mime) { case "text/html": case "text/css": case "text/javascript": case "text/xml": case "application/css": case "application/javascript": case "application/xml": $mime = "text/plain"; break; } header("Content-Type: " . $mime); header("Content-Length: " . strlen($attachment->contents)); header("Cache-Control: no-cache"); header("Content-Disposition: inline; filename=\"" . $name . "\""); echo $attachment->contents; } elseif ($_action === "profilepicture") { $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; } $ifNoneMatch = $_SERVER["HTTP_IF_NONE_MATCH"] ?? null; if ($ifNoneMatch !== null) $ifNoneMatch = trim($ifNoneMatch, '"'); if ($user->profilePicture === null) { $fallback = __DIR__ . "/application/assets/user-fallback.jpg"; $etag = md5("\0"); header("Content-Type: image/jpeg"); header("Content-Length: " . filesize($fallback)); header("Cache-Control: no-cache"); header("ETag: \"" . $etag . "\""); if ($ifNoneMatch === $etag) http_response_code(304); else readfile($fallback); } else { $etag = md5($user->profilePicture); header("Content-Type: image/jpeg"); header("Content-Length: " . strlen($user->profilePicture)); header("Cache-Control: no-cache"); header("ETag: \"" . $etag . "\""); if ($ifNoneMatch === $etag) http_response_code(304); else echo $user->profilePicture; } } 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; } $contentHash = hash("sha256", $attachment->contents); $cacheId = bin2hex($attachment->id); $cacheDir = sys_get_temp_dir() . "/mystic/forum/0/cache/thumbs/" . substr($cacheId, 0, 2) . "/" . substr($cacheId, 0, 8) . "/"; if (!is_dir($cacheDir)) mkdir($cacheDir, recursive: true); $cacheFileData = $cacheDir . $cacheId . ".data"; $cacheFileInfo = $cacheDir . $cacheId . ".info"; if (is_file($cacheFileData) && is_file($cacheFileInfo)) { $info = json_decode(file_get_contents($cacheFileInfo)); if ($info->contentHash === $contentHash) { header("Content-Type: image/jpeg"); header("Cache-Control: max-age=86400"); header("X-Debug-Content: $cacheFileData"); readfile($cacheFileData); exit; } } $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: max-age=86400"); imagejpeg($thumb, $cacheFileData, 40); imagedestroy($thumb); file_put_contents($cacheFileInfo, json_encode([ "format" => 1, "contentHash" => $contentHash, "created" => time(), ], JSON_UNESCAPED_SLASHES)); readfile($cacheFileData); } 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) || $post->deleted) { http_response_code(404); Messaging::error("No post exists with this id"); exit; } $topicAuthor = new User(); $topicAuthor->id = $post->authorId; if (!$db->fetch($topicAuthor)) $topicAuthor = null; $canEdit = ($currentUser->id === $topicAuthor?->id && $topicAuthor?->hasPermission(UserPermissions::DELETE_OWN_POST)) || ($currentUser->hasPermission(UserPermissions::DELETE_OTHER_POST)); if (!$canEdit) { http_response_code(403); Messaging::error("You don't have permission to delete this post"); exit; } $attachments = $db->fetchCustom(Attachment::class, 'WHERE post_id = $1', [ $post->id ]); $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; } 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" => __("Delete post")]); _view("template_navigation_start"); _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]); _view("template_navigation_end"); _view("form_delete_post_confirm", [ "post" => $post, "postAuthor" => $topicAuthor, "attachments" => $attachments, ]); _view("template_end"); } } elseif ($_action === "updatepost") { RequestUtils::ensureRequestMethod("POST"); if (!$currentUser) { http_response_code(403); Messaging::error("You need to be logged in to update posts!"); exit; } $postId = RequestUtils::getRequiredField("post"); $message = RequestUtils::getRequiredField("message"); $post = new Post(); $post->id = $postId; if (!$db->fetch($post) || $post->deleted) { http_response_code(404); Messaging::error("No post exists with this id"); exit; } $topicAuthor = new User(); $topicAuthor->id = $post->authorId; if (!$db->fetch($topicAuthor)) $topicAuthor = null; $canEdit = ($currentUser->id === $topicAuthor?->id && $topicAuthor?->hasPermission(UserPermissions::EDIT_OWN_POST)) || ($currentUser->hasPermission(UserPermissions::EDIT_OTHER_POST)); if (!$canEdit) { http_response_code(403); Messaging::error("You don't have permission to edit this post"); exit; } $confirm = $_POST["confirm"] ?? null; $post->content = $message; $post->edited = true; if (!$db->update($post)) { http_response_code(500); Messaging::error("Failed to update post"); exit; } header("Location: ?_action=viewtopic&topic=" . urlencode($post->topicId) . "#post-" . urlencode($postId)); } elseif ($_action === "deletetopic") { RequestUtils::ensureRequestMethod("POST"); if (!$currentUser) { http_response_code(403); Messaging::error("You need to be logged in to delete topics!"); exit; } $topicId = RequestUtils::getRequiredField("topic"); $topic = new Topic(); $topic->id = $topicId; if (!$db->fetch($topic)) { http_response_code(404); Messaging::error("No topic exists with this id"); exit; } $topicAuthor = new User(); $topicAuthor->id = $topic->createdBy; if (!$db->fetch($topicAuthor)) $topicAuthor = null; $canEdit = ($currentUser->id === $topicAuthor?->id && $topicAuthor?->hasPermission(UserPermissions::DELETE_OWN_TOPIC)) || ($currentUser->hasPermission(UserPermissions::DELETE_OTHER_TOPIC)); if (!$canEdit) { http_response_code(403); Messaging::error("You don't have permission to delete this topic"); exit; } $confirm = $_POST["confirm"] ?? null; if ($confirm !== null) { $expectedConfirm = base64_encode(hash("sha256", "confirm" . $topic->id, true)); if ($confirm !== $expectedConfirm) { http_response_code(400); Messaging::error("Invalid confirmation"); exit; } if (!$db->delete($topic)) { http_response_code(500); Messaging::error("Failed to delete topic"); exit; } header("Location: ."); } else { _view("template_start", ["_title" => "Delete topic"]); _view("template_navigation_start"); _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]); _view("template_navigation_end"); _view("form_delete_topic_confirm", [ "topic" => $topic, "topicAuthor" => $topicAuthor, ]); _view("template_end"); } } elseif ($_action === "updatetopic") { RequestUtils::ensureRequestMethod("POST"); if (!$currentUser) { http_response_code(403); Messaging::error("You need to be logged in to update topics!"); exit; } $topicId = RequestUtils::getRequiredField("topic"); $title = RequestUtils::getRequiredField("title"); $topic = new Topic(); $topic->id = $topicId; if (!$db->fetch($topic)) { http_response_code(404); Messaging::error("No topic exists with this id"); exit; } $topicAuthor = new User(); $topicAuthor->id = $topic->createdBy; if (!$db->fetch($topicAuthor)) $topicAuthor = null; $canEdit = ($currentUser->id === $topicAuthor?->id && $topicAuthor?->hasPermission(UserPermissions::EDIT_OWN_TOPIC)) || ($currentUser->hasPermission(UserPermissions::EDIT_OTHER_TOPIC)); if (!$canEdit) { http_response_code(403); Messaging::error("You don't have permission to update this topic"); exit; } $topic->title = $title; if (!$db->update($topic)) { http_response_code(500); Messaging::error("Failed to update topic"); exit; } header("Location: ./?_action=viewtopic&topic=" . urlencode($topicId)); } elseif ($_action === "captcha") { $phrase = generateCaptchaText(); $builder = new CaptchaBuilder($phrase); $builder->build(192, 48); $_SESSION["captchaPhrase"] = $phrase; header("Content-Type: image/jpeg"); header("Pragma: no-cache"); header("Cache-Control: no-cache"); $builder->save(null, 40); } elseif ($_action === null) { _view("template_start"); _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"); }