diff options
| author | Jonas Kohl | 2024-09-12 19:49:17 +0200 | 
|---|---|---|
| committer | Jonas Kohl | 2024-09-12 19:49:17 +0200 | 
| commit | 086e2d2668784469ec114f6e6fd2b3dace3d7c3b (patch) | |
| tree | b9bacedb713501d88d24085940267a7c94e69b29 /src/index.php | |
| parent | 34b1b391d4b03659a96f868857c230002b351514 (diff) | |
Way more progress on forum
Diffstat (limited to 'src/index.php')
| -rw-r--r-- | src/index.php | 509 | 
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, "> ")) { +            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"); |