From 0633a48f09611e9f9b1bdeae79fd8127513c7b3e Mon Sep 17 00:00:00 2001 From: flashwave <me@flash.moe> Date: Wed, 21 Oct 2020 22:33:21 +0000 Subject: [PATCH] // whoa i can't believe it's more progress! --- misuzu.php | 3 - public/forum/post.php | 157 ++++-------- public/forum/posting.php | 52 ++-- public/forum/topic.php | 31 +-- public/search.php | 3 +- src/Console/Commands/CronCommand.php | 12 +- src/Forum/ForumCategory.php | 76 +++++- src/Forum/ForumPost.php | 178 ++++++++++++- src/Forum/ForumTopic.php | 36 ++- src/Forum/ForumTopicTrack.php | 52 ++++ src/Forum/forum.php | 147 ----------- src/Forum/perms.php | 21 ++ src/Forum/post.php | 358 --------------------------- src/Forum/topic.php | 56 ----- src/Users/User.php | 1 + templates/forum/macros.twig | 8 +- templates/forum/posting.twig | 24 +- 17 files changed, 456 insertions(+), 759 deletions(-) delete mode 100644 src/Forum/forum.php delete mode 100644 src/Forum/post.php delete mode 100644 src/Forum/topic.php diff --git a/misuzu.php b/misuzu.php index 5d3d0166..b8f30afd 100644 --- a/misuzu.php +++ b/misuzu.php @@ -76,9 +76,6 @@ require_once 'src/perms.php'; require_once 'src/manage.php'; require_once 'src/url.php'; require_once 'src/Forum/perms.php'; -require_once 'src/Forum/forum.php'; -require_once 'src/Forum/post.php'; -require_once 'src/Forum/topic.php'; $dbConfig = parse_ini_file(MSZ_CONFIG . '/config.ini', true, INI_SCANNER_TYPED); diff --git a/public/forum/post.php b/public/forum/post.php index 7a559873..fecc9903 100644 --- a/public/forum/post.php +++ b/public/forum/post.php @@ -2,6 +2,8 @@ namespace Misuzu; use Misuzu\AuditLog; +use Misuzu\Forum\ForumPost; +use Misuzu\Forum\ForumPostNotFoundException; use Misuzu\Users\User; use Misuzu\Users\UserSession; @@ -55,59 +57,34 @@ if($isXHR) { header(CSRF::header()); } -$postInfo = forum_post_get($postId, true); -$perms = empty($postInfo) - ? 0 - : forum_perms_get_user($postInfo['forum_id'], $currentUserId)[MSZ_FORUM_PERMS_GENERAL]; +try { + $postInfo = ForumPost::byId($postId); + $perms = forum_perms_get_user($postInfo->getCategoryId(), $currentUserId)[MSZ_FORUM_PERMS_GENERAL]; +} catch(ForumPostNotFoundException $ex) { + $postInfo = null; + $perms = 0; +} switch($postMode) { case 'delete': - $canDelete = forum_post_can_delete($postInfo, $currentUserId); - $canDeleteMsg = ''; - $responseCode = 200; + $canDeleteCodes = [ + 'view' => 404, + 'deleted' => 404, + 'owner' => 403, + 'age' => 403, + 'permission' => 403, + '' => 200, + ]; + $canDelete = $postInfo->canBeDeleted($currentUser); + $canDeleteMsg = ForumPost::canBeDeletedErrorString($canDelete); + $responseCode = $canDeleteCodes[$canDelete] ?? 500; - switch($canDelete) { - case MSZ_E_FORUM_POST_DELETE_USER: // i don't think this is ever reached but we may as well have it - $responseCode = 401; - $canDeleteMsg = 'You must be logged in to delete posts.'; - break; - case MSZ_E_FORUM_POST_DELETE_POST: - $responseCode = 404; - $canDeleteMsg = "This post doesn't exist."; - break; - case MSZ_E_FORUM_POST_DELETE_DELETED: - $responseCode = 404; - $canDeleteMsg = 'This post has already been marked as deleted.'; - break; - case MSZ_E_FORUM_POST_DELETE_OWNER: - $responseCode = 403; - $canDeleteMsg = 'You can only delete your own posts.'; - break; - case MSZ_E_FORUM_POST_DELETE_OLD: - $responseCode = 401; - $canDeleteMsg = 'This post has existed for too long. Ask a moderator to remove if it absolutely necessary.'; - break; - case MSZ_E_FORUM_POST_DELETE_PERM: - $responseCode = 401; - $canDeleteMsg = 'You are not allowed to delete posts.'; - break; - case MSZ_E_FORUM_POST_DELETE_OP: - $responseCode = 403; - $canDeleteMsg = 'This is the opening post of a topic, it may not be deleted without deleting the entire topic as well.'; - break; - case MSZ_E_FORUM_POST_DELETE_OK: - break; - default: - $responseCode = 500; - $canDeleteMsg = sprintf('Unknown error \'%d\'', $canDelete); - } - - if($canDelete !== MSZ_E_FORUM_POST_DELETE_OK) { + if($canDelete !== '') { if($isXHR) { http_response_code($responseCode); echo json_encode([ 'success' => false, - 'post_id' => $postInfo['post_id'], + 'post_id' => $postInfo->getId(), 'code' => $canDelete, 'message' => $canDeleteMsg, ]); @@ -121,17 +98,17 @@ switch($postMode) { if(!$isXHR) { if($postRequestVerified && !$submissionConfirmed) { url_redirect('forum-post', [ - 'post' => $postInfo['post_id'], - 'post_fragment' => 'p' . $postInfo['post_id'], + 'post' => $postInfo->getId(), + 'post_fragment' => 'p' . $postInfo->getId(), ]); break; } elseif(!$postRequestVerified) { Template::render('forum.confirm', [ 'title' => 'Confirm post deletion', 'class' => 'far fa-trash-alt', - 'message' => sprintf('You are about to delete post #%d. Are you sure about that?', $postInfo['post_id']), + 'message' => sprintf('You are about to delete post #%d. Are you sure about that?', $postInfo->getId()), 'params' => [ - 'p' => $postInfo['post_id'], + 'p' => $postInfo->getId(), 'm' => 'delete', ], ]); @@ -139,16 +116,13 @@ switch($postMode) { } } - $deletePost = forum_post_delete($postInfo['post_id']); - - if($deletePost) { - AuditLog::create(AuditLog::FORUM_POST_DELETE, [$postInfo['post_id']]); - } + $postInfo->delete(); + AuditLog::create(AuditLog::FORUM_POST_DELETE, [$postInfo->getId()]); if($isXHR) { echo json_encode([ 'success' => $deletePost, - 'post_id' => $postInfo['post_id'], + 'post_id' => $postInfo->getId(), 'message' => $deletePost ? 'Post deleted!' : 'Failed to delete post.', ]); break; @@ -159,7 +133,7 @@ switch($postMode) { break; } - url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]); + url_redirect('forum-topic', ['topic' => $postInfo->getTopicId()]); break; case 'nuke': @@ -171,17 +145,17 @@ switch($postMode) { if(!$isXHR) { if($postRequestVerified && !$submissionConfirmed) { url_redirect('forum-post', [ - 'post' => $postInfo['post_id'], - 'post_fragment' => 'p' . $postInfo['post_id'], + 'post' => $postInfo->getId(), + 'post_fragment' => 'p' . $postInfo->getId(), ]); break; } elseif(!$postRequestVerified) { Template::render('forum.confirm', [ 'title' => 'Confirm post nuke', 'class' => 'fas fa-radiation', - 'message' => sprintf('You are about to PERMANENTLY DELETE post #%d. Are you sure about that?', $postInfo['post_id']), + 'message' => sprintf('You are about to PERMANENTLY DELETE post #%d. Are you sure about that?', $postInfo->getId()), 'params' => [ - 'p' => $postInfo['post_id'], + 'p' => $postInfo->getId(), 'm' => 'nuke', ], ]); @@ -189,18 +163,12 @@ switch($postMode) { } } - $nukePost = forum_post_nuke($postInfo['post_id']); - - if(!$nukePost) { - echo render_error(500); - break; - } - - AuditLog::create(AuditLog::FORUM_POST_NUKE, [$postInfo['post_id']]); + $postInfo->nuke(); + AuditLog::create(AuditLog::FORUM_POST_NUKE, [$postInfo->getId()]); http_response_code(204); if(!$isXHR) { - url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]); + url_redirect('forum-topic', ['topic' => $postInfo->getTopicId()]); } break; @@ -213,17 +181,17 @@ switch($postMode) { if(!$isXHR) { if($postRequestVerified && !$submissionConfirmed) { url_redirect('forum-post', [ - 'post' => $postInfo['post_id'], - 'post_fragment' => 'p' . $postInfo['post_id'], + 'post' => $postInfo->getId(), + 'post_fragment' => 'p' . $postInfo->getId(), ]); break; } elseif(!$postRequestVerified) { Template::render('forum.confirm', [ 'title' => 'Confirm post restore', 'class' => 'fas fa-magic', - 'message' => sprintf('You are about to restore post #%d. Are you sure about that?', $postInfo['post_id']), + 'message' => sprintf('You are about to restore post #%d. Are you sure about that?', $postInfo->getId()), 'params' => [ - 'p' => $postInfo['post_id'], + 'p' => $postInfo->getId(), 'm' => 'restore', ], ]); @@ -231,49 +199,12 @@ switch($postMode) { } } - $restorePost = forum_post_restore($postInfo['post_id']); - - if(!$restorePost) { - echo render_error(500); - break; - } - - AuditLog::create(AuditLog::FORUM_POST_RESTORE, [$postInfo['post_id']]); + $postInfo->restore(); + AuditLog::create(AuditLog::FORUM_POST_RESTORE, [$postInfo->getId()]); http_response_code(204); if(!$isXHR) { - url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]); + url_redirect('forum-topic', ['topic' => $postInfo->getTopicId()]); } break; - - default: // function as an alt for topic.php?p= by default - $canDeleteAny = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST); - - if(!empty($postInfo['post_deleted']) && !$canDeleteAny) { - echo render_error(404); - break; - } - - $postFind = forum_post_find($postInfo['post_id'], $currentUserId); - - if(empty($postFind)) { - echo render_error(404); - break; - } - - if($canDeleteAny) { - $postInfo['preceeding_post_count'] += $postInfo['preceeding_post_deleted_count']; - } - - unset($postInfo['preceeding_post_deleted_count']); - - if($isXHR) { - echo json_encode($postFind); - break; - } - - url_redirect('forum-topic', [ - 'topic' => $postFind['topic_id'], - 'page' => floor($postFind['preceeding_post_count'] / \Misuzu\Forum\ForumPost::PER_PAGE) + 1, - ]); } diff --git a/public/forum/posting.php b/public/forum/posting.php index 729dd5b9..70326428 100644 --- a/public/forum/posting.php +++ b/public/forum/posting.php @@ -8,6 +8,8 @@ use Misuzu\Forum\ForumTopicNotFoundException; use Misuzu\Forum\ForumTopicCreationFailedException; use Misuzu\Forum\ForumTopicUpdateFailedException; use Misuzu\Forum\ForumPost; +use Misuzu\Forum\ForumPostCreationFailedException; +use Misuzu\Forum\ForumPostUpdateFailedException; use Misuzu\Forum\ForumPostNotFoundException; use Misuzu\Net\IPAddress; use Misuzu\Parsers\Parser; @@ -71,13 +73,11 @@ if(empty($postId) && empty($topicId) && empty($forumId)) { return; } -if(!empty($postId)) { - $post = forum_post_get($postId); - - if(isset($post['topic_id'])) { // should automatic cross-quoting be a thing? if so, check if $topicId is < 1 first - $topicId = (int)$post['topic_id']; - } -} +if(!empty($postId)) + try { + $postInfo = ForumPost::byId($postId); + $topicId = $postInfo->getTopicId(); + } catch(ForumPostNotFoundException $ex) {} if(!empty($topicId)) try { @@ -122,12 +122,12 @@ if($mode === 'create' || $mode === 'edit') { // edit mode stuff if($mode === 'edit') { - if(empty($post)) { + if(empty($postInfo)) { echo render_error(404); return; } - if(!perms_check($perms, $post['poster_id'] === $currentUserId ? MSZ_FORUM_PERM_EDIT_POST : MSZ_FORUM_PERM_EDIT_ANY_POST)) { + if(!perms_check($perms, $postInfo->getUserId() === $currentUserId ? MSZ_FORUM_PERM_EDIT_POST : MSZ_FORUM_PERM_EDIT_ANY_POST)) { echo render_error(403); return; } @@ -140,16 +140,16 @@ if(!empty($_POST)) { $topicTitle = $_POST['post']['title'] ?? ''; $postText = $_POST['post']['text'] ?? ''; $postParser = (int)($_POST['post']['parser'] ?? Parser::BBCODE); - $topicType = isset($_POST['post']['type']) ? (int)$_POST['post']['type'] : null; + $topicType = isset($_POST['post']['type']) ? (int)$_POST['post']['type'] : ForumTopic::TYPE_DISCUSSION; $postSignature = isset($_POST['post']['signature']); if(!CSRF::validateRequest()) { $notices[] = 'Could not verify request.'; } else { - $isEditingTopic = $isNewTopic || ($mode === 'edit' && $post['is_opening_post']); + $isEditingTopic = $isNewTopic || ($mode === 'edit' && $postInfo->isOpeningPost()); if($mode === 'create') { - $timeoutCheck = max(1, forum_timeout($forumInfo->getId(), $currentUserId)); + $timeoutCheck = max(1, $forumInfo->checkCooldown($currentUser)); if($timeoutCheck < 5) { $notices[] = sprintf("You're posting too quickly! Please wait %s seconds before posting again.", number_format($timeoutCheck)); @@ -192,21 +192,25 @@ if(!empty($_POST)) { $topicId = $topicInfo->getId(); } - $postId = forum_post_create( - $topicId, - $forumInfo->getId(), - $currentUserId, - IPAddress::remote(), - $postText, - $postParser, - $postSignature - ); - forum_topic_mark_read($currentUserId, $topicId, $forumInfo->getId()); + $postInfo = ForumPost::create($topicInfo, $currentUser, IPAddress::remote(), $postText, $postParser, $postSignature); + $postId = $postInfo->getId(); + + $topicInfo->markRead($currentUser); $forumInfo->increaseTopicPostCount($isNewTopic); break; case 'edit': - if(!forum_post_update($postId, IPAddress::remote(), $postText, $postParser, $postSignature, $postText !== $post['post_text'])) { + if($postText !== $postInfo->getBody() && $postInfo->shouldBumpEdited()) + $postInfo->bumpEdited(); + + $postInfo->setRemoteAddress(IPAddress::remote()) + ->setBody($postText) + ->setBodyParser($postParser) + ->setDisplaySignature($postSignature); + + try { + $postInfo->update(); + } catch(ForumPostUpdateFailedException $ex) { $notices[] = 'Post edit failed.'; } @@ -240,7 +244,7 @@ if(!$isNewTopic && !empty($topicInfo)) { } if($mode === 'edit') { // $post is pretty much sure to be populated at this point - Template::set('posting_post', $post); + Template::set('posting_post', $postInfo); } Template::render('forum.posting', [ diff --git a/public/forum/topic.php b/public/forum/topic.php index 6c851b37..cee90e80 100644 --- a/public/forum/topic.php +++ b/public/forum/topic.php @@ -19,12 +19,14 @@ $submissionConfirmed = filter_input(INPUT_GET, 'confirm') === '1'; $topicUser = User::getCurrent(); $topicUserId = $topicUser === null ? 0 : $topicUser->getId(); -if($topicId < 1 && $postId > 0) { - $postInfo = forum_post_find($postId, $topicUserId); - - if(!empty($postInfo['topic_id'])) - $topicId = (int)$postInfo['topic_id']; -} +if($topicId < 1 && $postId > 0) + try { + $postInfo = ForumPost::byId($postId); + $topicId = $postInfo->getTopicId(); + } catch(ForumPostNotFoundException $ex) { + echo render_error(404); + return; + } try { $topicInfo = ForumTopic::byId($topicId); @@ -113,8 +115,8 @@ if(in_array($moderationMode, $validModerationModes, true)) { 'posts' => 403, '' => 200, ]; - $canDelete = $topicInfo->canDelete($topicUser); - $canDeleteMsg = ForumTopic::canDeleteErrorString($canDelete); + $canDelete = $topicInfo->canBeDeleted($topicUser); + $canDeleteMsg = ForumTopic::canBeDeletedErrorString($canDelete); $responseCode = $canDeleteCodes[$canDelete] ?? 500; if($canDelete !== '') { @@ -283,15 +285,8 @@ if(in_array($moderationMode, $validModerationModes, true)) { $topicPagination = new Pagination($topicInfo->getActualPostCount($canDeleteAny), \Misuzu\Forum\ForumPost::PER_PAGE, 'page'); -if(isset($postInfo['preceeding_post_count'])) { - $preceedingPosts = $postInfo['preceeding_post_count']; - - if($canDeleteAny) { - $preceedingPosts += $postInfo['preceeding_post_deleted_count']; - } - - $topicPagination->setPage(floor($preceedingPosts / $topicPagination->getRange()), true); -} +if(isset($postInfo)) + $topicPagination->setPage($postInfo->getTopicPage($canDeleteAny, $topicPagination->getRange())); if(!$topicPagination->hasValidOffset()) { echo render_error(404); @@ -300,7 +295,7 @@ if(!$topicPagination->hasValidOffset()) { $canReply = !$topicInfo->isArchived() && !$topicInfo->isLocked() && !$topicInfo->isDeleted() && perms_check($perms, MSZ_FORUM_PERM_CREATE_POST); -forum_topic_mark_read($topicUserId, $topicInfo->getId(), $topicInfo->getCategoryId()); +$topicInfo->markRead($topicUser); Template::render('forum.topic', [ 'topic_perms' => $perms, diff --git a/public/search.php b/public/search.php index d8ca137b..2f834003 100644 --- a/public/search.php +++ b/public/search.php @@ -2,6 +2,7 @@ namespace Misuzu; use Misuzu\Forum\ForumTopic; +use Misuzu\Forum\ForumPost; use Misuzu\News\NewsPost; use Misuzu\Users\User; @@ -11,7 +12,7 @@ $searchQuery = !empty($_GET['q']) && is_string($_GET['q']) ? $_GET['q'] : ''; if(!empty($searchQuery)) { $forumTopics = ForumTopic::bySearchQuery($searchQuery); - $forumPosts = forum_post_search($searchQuery); + $forumPosts = ForumPost::bySearchQuery($searchQuery); $newsPosts = NewsPost::bySearchQuery($searchQuery); $findUsers = DB::prepare(sprintf( diff --git a/src/Console/Commands/CronCommand.php b/src/Console/Commands/CronCommand.php index b9299a5a..7e06463d 100644 --- a/src/Console/Commands/CronCommand.php +++ b/src/Console/Commands/CronCommand.php @@ -28,11 +28,19 @@ class CronCommand implements CommandInterface { case 'func': call_user_func($task['command']); break; + + case 'selffunc': + call_user_func(self::class . '::' . $task['command']); + break; } } } } + private static function syncForum(): void { + \Misuzu\Forum\ForumCategory::root()->synchronise(true); + } + private const TASKS = [ [ 'name' => 'Ensures main role exists.', @@ -147,9 +155,9 @@ class CronCommand implements CommandInterface { ], [ 'name' => 'Recount forum topics and posts.', - 'type' => 'func', + 'type' => 'selffunc', 'slow' => true, - 'command' => 'forum_count_synchronise', + 'command' => 'syncForum', ], [ 'name' => 'Clean up expired tfa tokens.', diff --git a/src/Forum/ForumCategory.php b/src/Forum/ForumCategory.php index 0d4675cc..ff8fe6a8 100644 --- a/src/Forum/ForumCategory.php +++ b/src/Forum/ForumCategory.php @@ -351,15 +351,77 @@ class ForumCategory { return $this->checkLegacyPermission($user, MSZ_FORUM_PERM_SET_READ); } - public function hasUnread(?User $user): bool { - if($user === null) - return false; - return forum_topics_unread($this->getId(), $user->getId()); + public function hasRead(User $user): bool { + static $cache = []; + + $cacheId = $user->getId() . ':' . $this->getId(); + if(isset($cache[$cacheId])) + return $cache[$cacheId]; + + if(!$this->canView($user)) + return $cache[$cacheId] = true; + + $countUnread = (int)DB::prepare( + 'SELECT COUNT(*) FROM `' . DB::PREFIX . ForumTopic::TABLE . '` AS ti' + . ' LEFT JOIN `' . DB::PREFIX . ForumTopicTrack::TABLE . '` AS tt' + . ' ON tt.`topic_id` = ti.`topic_id` AND tt.`user_id` = :user' + . ' WHERE ti.`forum_id` = :forum AND ti.`topic_deleted` IS NULL' + . ' AND ti.`topic_bumped` >= NOW() - INTERVAL :limit SECOND' + . ' AND (tt.`track_last_read` IS NULL OR tt.`track_last_read` < ti.`topic_bumped`)' + )->bind('forum', $this->getId()) + ->bind('user', $user->getId()) + ->bind('limit', ForumTopic::UNREAD_TIME_LIMIT) + ->fetchColumn(); + + if($countUnread > 0) + return $cache[$cacheId] = false; + + foreach($this->getChildren() as $child) + if(!$child->hasRead($user)) + return $cache[$cacheId] = false; + + return $cache[$cacheId] = true; } + public function markAsRead(User $user, bool $recursive = true): void { - // Recursion is implied for now - // Also forego recursion if we're root and just mark the entire forum as expected - forum_mark_read($this->isRoot() ? null : $this->getId(), $user->getId()); + if($this->isRoot()) { + if(!$recursive) + return; + $recursive = false; + } + + if($recursive) { + $children = $this->getChildren($user); + foreach($children as $child) + $child->markAsRead($user, true); + } + + $mark = DB::prepare( + 'INSERT INTO `' . DB::PREFIX . ForumTopicTrack::TABLE . '`' + . ' (`user_id`, `topic_id`, `forum_id`, `track_last_read`)' + . ' SELECT u.`user_id`, t.`topic_id`, t.`forum_id`, NOW()' + . ' FROM `msz_forum_topics` AS t' + . ' LEFT JOIN `msz_users` AS u ON u.`user_id` = :user' + . ' WHERE t.`topic_deleted` IS NULL' + . ' AND t.`topic_bumped` >= NOW() - INTERVAL :limit SECOND' + . ($this->isRoot() ? '' : ' AND t.`forum_id` = :forum') + . ' GROUP BY t.`topic_id`' + . ' ON DUPLICATE KEY UPDATE `track_last_read` = NOW()' + )->bind('user', $user->getId()) + ->bind('limit', ForumTopic::UNREAD_TIME_LIMIT); + + if(!$this->isRoot()) + $mark->bind('forum', $this->getId()); + + $mark->execute(); + } + + public function checkCooldown(User $user): int { + return (int)DB::prepare( + 'SELECT TIMESTAMPDIFF(SECOND, COALESCE(MAX(`post_created`), NOW() - INTERVAL 1 YEAR), NOW())' + . ' FROM `' . DB::PREFIX . ForumPost::TABLE . '`' + . ' WHERE `forum_id` = :forum AND `user_id` = :user' + )->bind('forum', $this->getId())->bind('user', $user->getId())->fetchColumn(); } public function getLatestTopic(?User $viewer = null): ?ForumTopic { diff --git a/src/Forum/ForumPost.php b/src/Forum/ForumPost.php index 2fcc6d1a..86b9b480 100644 --- a/src/Forum/ForumPost.php +++ b/src/Forum/ForumPost.php @@ -11,6 +11,7 @@ use Misuzu\Users\UserNotFoundException; class ForumPostException extends ForumException {} class ForumPostNotFoundException extends ForumPostException {} class ForumPostCreationFailedException extends ForumPostException {} +class ForumPostUpdateFailedException extends ForumPostException {} class ForumPost { public const PER_PAGE = 10; @@ -18,6 +19,10 @@ class ForumPost { public const BODY_MIN_LENGTH = 1; public const BODY_MAX_LENGTH = 60000; + public const EDIT_BUMP_THRESHOLD = 60 * 5; + + public const DELETE_AGE_LIMIT = 60 * 60 * 24 * 7; + // Database fields private $post_id = -1; private $topic_id = -1; @@ -117,6 +122,10 @@ class ForumPost { public function getRemoteAddress(): string { return $this->post_ip; } + public function setRemoteAddress(string $remoteAddress): self { + $this->post_ip = $remoteAddress; + return $this; + } public function getBody(): string { return $this->post_text; @@ -157,6 +166,12 @@ class ForumPost { public function getCreatedTime(): int { return $this->post_created === null ? -1 : $this->post_created; } + public function getAge(): int { + return time() - $this->getCreatedTime(); + } + public function shouldBumpEdited(): bool { + return $this->getAge() > self::EDIT_BUMP_THRESHOLD; + } public function getEditedTime(): int { return $this->post_edited === null ? -1 : $this->post_edited; @@ -188,6 +203,17 @@ class ForumPost { return $this->getTopic()->isTopicAuthor($this->getUser()); } + public function getTopicOffset(bool $includeDeleted = false): int { + return (int)DB::prepare( + 'SELECT COUNT(`post_id`) FROM `' . DB::PREFIX . self::TABLE . '`' + . ' WHERE `topic_id` = :topic AND `post_id` < :post' + . ($includeDeleted ? '' : ' AND `post_deleted` IS NULL') + )->bind('topic', $this->getTopicId())->bind('post', $this->getId())->fetchColumn(); + } + public function getTopicPage(bool $includeDeleted = false, int $postsPerPage = self::PER_PAGE): int { + return floor($this->getTopicOffset() / $postsPerPage) + 1; + } + public function canBeSeen(?User $user): bool { if($user === null && $this->isDeleted()) return false; @@ -202,13 +228,6 @@ class ForumPost { return $this->getUser()->getId() === $user->getId(); } - // complete this implementation - public function canBeDeleted(?User $user): bool { - if($user === null) - return false; - return $this->getUser()->getId() === $user->getId(); - } - public static function validateBody(string $body): string { $length = mb_strlen(trim($body)); if($length < self::BODY_MIN_LENGTH) @@ -230,6 +249,73 @@ class ForumPost { } } + public function canBeDeleted(User $user): string { + if(false) // check if viewable + return 'view'; + + if($this->isOpeningPost()) + return 'opening'; + + // check if user can view deleted posts/is mod + $canDeleteAny = false; + + if($this->isDeleted()) + return $canDeleteAny ? 'deleted' : 'view'; + + if(!$canDeleteAny) { + if(false) // check if user can delete posts + return 'permission'; + if($user->getId() !== $this->getUserId()) + return 'owner'; + if($this->getCreatedTime() <= time() - self::DELETE_AGE_LIMIT) + return 'age'; + } + + return ''; + } + public static function canBeDeletedErrorString(string $error): string { + switch($error) { + case 'view': + return 'This post doesn\'t exist.'; + case 'deleted': + return 'This post has already been marked as deleted.'; + case 'permission': + return 'You aren\'t allowed to this post.'; + case 'owner': + return 'You can only delete your own posts.'; + case 'age': + return 'This post is too old to be deleted. Ask a moderator to remove it if you deem it absolutely necessary.'; + case '': + return 'Post can be deleted!'; + default: + return 'Post cannot be deleted.'; + } + } + + public function delete(): void { + if($this->isDeleted()) + return; + $this->post_deleted = time(); + DB::prepare('UPDATE `' . DB::PREFIX . self::TABLE . '` SET `post_deleted` = NOW() WHERE `post_id` = :post') + ->bind('post', $this->getId()) + ->execute(); + } + public function restore(): void { + if(!$this->isDeleted()) + return; + $this->post_deleted = null; + DB::prepare('UPDATE `' . DB::PREFIX . self::TABLE . '` SET `post_deleted` = NULL WHERE `post_id` = :post') + ->bind('post', $this->getId()) + ->execute(); + } + public function nuke(): void { + if(!$this->isDeleted()) + return; + DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `post_id` = :post') + ->bind('post', $this->getId()) + ->execute(); + } + public static function deleteTopic(ForumTopic $topic): void { // Deleting posts should only be possible while the topic is already in a deleted state if(!$topic->isDeleted()) @@ -261,6 +347,62 @@ class ForumPost { ->execute(); } + public static function create( + ForumTopic $topic, + User $user, + string $ipAddress, + string $text, + int $parser = Parser::PLAIN, + bool $displaySignature = true + ): ForumPost { + $create = DB::prepare( + 'INSERT INTO `msz_forum_posts` (' + . '`topic_id`, `forum_id`, `user_id`, `post_ip`, `post_text`, `post_parse`, `post_display_signature`' + . ') VALUES (:topic, :forum, :user, INET6_ATON(:ip), :body, :parser, :display_signature)' + )->bind('topic', $topic->getId()) + ->bind('forum', $topic->getCategoryId()) + ->bind('user', $user->getId()) + ->bind('ip', $ipAddress) + ->bind('body', $text) + ->bind('parser', $parser) + ->bind('display_signature', $displaySignature ? 1 : 0) + ->execute(); + if(!$create) + throw new ForumPostCreationFailedException; + + $postId = DB::lastId(); + if($postId < 1) + throw new ForumPostCreationFailedException; + + try { + return self::byId($postId); + } catch(ForumPostNotFoundException $ex) { + throw new ForumPostCreationFailedException; + } + } + + public function update(): void { + if($this->getId() < 1) + throw new ForumPostUpdateFailedException; + + if(!DB::prepare( + 'UPDATE `' . DB::PREFIX . self::TABLE . '`' + . ' SET `post_ip` = INET6_ATON(:ip),' + . ' `post_text` = :body,' + . ' `post_parse` = :parser,' + . ' `post_display_signature` = :display_signature,' + . ' `post_edited` = FROM_UNIXTIME(:edited)' + . ' WHERE `post_id` = :post' + )->bind('post', $this->getId()) + ->bind('ip', $this->getRemoteAddress()) + ->bind('body', $this->getBody()) + ->bind('parser', $this->getBodyParser()) + ->bind('display_signature', $this->shouldDisplaySignature() ? 1 : 0) + ->bind('edited', $this->isEdited() ? $this->getEditedTime() : null) + ->execute()) + throw new ForumPostUpdateFailedException; + } + private static function countQueryBase(): string { return sprintf(self::QUERY_SELECT, sprintf('COUNT(*)', self::TABLE)); } @@ -321,4 +463,26 @@ class ForumPost { $memoizer->insert($objects[] = $object); return $objects; } + public static function bySearchQuery(string $search, bool $includeDeleted = false, ?Pagination $pagination = null): array { + $query = self::byQueryBase() + . ' WHERE MATCH(`post_text`) AGAINST (:search IN NATURAL LANGUAGE MODE)' + . ($includeDeleted ? '' : ' AND `post_deleted` IS NULL') + . ' ORDER BY `post_id`'; + + if($pagination !== null) + $query .= ' LIMIT :range OFFSET :offset'; + + $getObjects = DB::prepare($query) + ->bind('search', $search); + + if($pagination !== null) + $getObjects->bind('range', $pagination->getRange()) + ->bind('offset', $pagination->getOffset()); + + $objects = []; + $memoizer = self::memoizer(); + while($object = $getObjects->fetchObject(self::class)) + $memoizer->insert($objects[] = $object); + return $objects; + } } diff --git a/src/Forum/ForumTopic.php b/src/Forum/ForumTopic.php index d3373e94..b38dd2b9 100644 --- a/src/Forum/ForumTopic.php +++ b/src/Forum/ForumTopic.php @@ -43,6 +43,8 @@ class ForumTopic { public const DELETE_AGE_LIMIT = 60 * 60 * 24; public const DELETE_POST_LIMIT = 2; + public const UNREAD_TIME_LIMIT = 60 * 60 * 24 * 31; + // Database fields private $topic_id = -1; private $forum_id = -1; @@ -155,7 +157,7 @@ class ForumTopic { if($this->hasPriorityVoting()) return 'far fa-star fa-fw'; - return ($this->hasUnread($viewer) ? 'fas' : 'far') . ' fa-comment fa-fw'; + return ($viewer === null || $this->hasRead($viewer) ? 'far' : 'fas') . ' fa-comment fa-fw'; } public function getTitle(): string { @@ -180,6 +182,12 @@ class ForumTopic { public function getViewCount(): int { return $this->topic_count_views; } + public function incrementViewCount(): void { + ++$this->topic_count_views; + DB::prepare('UPDATE `msz_forum_topics` SET `topic_count_views` = `topic_count_views` + 1 WHERE `topic_id` = :topic') + ->bind('topic', $this->getId()) + ->execute(); + } public function getFirstPostId(): int { return $this->topic_post_first < 1 ? -1 : $this->topic_post_first; @@ -274,10 +282,24 @@ class ForumTopic { return $this->polls; } - public function hasUnread(?User $user): bool { - if($user === null) + public function isAbandoned(): bool { + return $this->getBumpedTime() < time() - self::UNREAD_TIME_LIMIT; + } + public function hasRead(User $user): bool { + if($this->isAbandoned()) + return true; + + try { + $trackInfo = ForumTopicTrack::byTopicAndUser($this, $user); + return $trackInfo->getReadTime() >= $this->getBumpedTime(); + } catch(ForumTopicTrackNotFoundException $ex) { return false; - return mt_rand(0, 10) >= 5; + } + } + public function markRead(User $user): void { + if(!$this->hasRead($user)) + $this->incrementViewCount(); + ForumTopicTrack::bump($this, $user); } public function hasParticipated(?User $user): bool { @@ -307,7 +329,7 @@ class ForumTopic { return $this->getCategory()->canView($user); } - public function canDelete(User $user): string { + public function canBeDeleted(User $user): string { if(false) // check if viewable return 'view'; @@ -330,7 +352,7 @@ class ForumTopic { return ''; } - public static function canDeleteErrorString(string $error): string { + public static function canBeDeletedErrorString(string $error): string { switch($error) { case 'view': return 'This topic doesn\'t exist.'; @@ -402,7 +424,7 @@ class ForumTopic { throw new ForumTopicUpdateFailedException; if(!DB::prepare( - 'UPDATE `msz_forum_topics`' + 'UPDATE `' . DB::PREFIX . self::TABLE . '`' . ' SET `topic_title` = :title,' . ' `topic_type` = :type' . ' WHERE `topic_id` = :topic' diff --git a/src/Forum/ForumTopicTrack.php b/src/Forum/ForumTopicTrack.php index fb2fb365..045f3264 100644 --- a/src/Forum/ForumTopicTrack.php +++ b/src/Forum/ForumTopicTrack.php @@ -2,8 +2,12 @@ namespace Misuzu\Forum; use Misuzu\DB; +use Misuzu\Memoizer; use Misuzu\Users\User; +class ForumTopicTrackException extends ForumException {} +class ForumTopicTrackNotFoundException extends ForumTopicTrackException {} + class ForumTopicTrack { // Database fields private $user_id = -1; @@ -50,4 +54,52 @@ class ForumTopicTrack { public function getReadTime(): int { return $this->track_last_read === null ? -1 : $this->track_last_read; } + + public static function bump(ForumTopic $topic, User $user): void { + DB::prepare( + 'REPLACE INTO `' . DB::PREFIX . self::TABLE . '`' + . ' (`user_id`, `topic_id`, `forum_id`, `track_last_read`)' + . ' VALUES (:user, :topic, :forum, NOW())' + )->bind('user', $user->getId()) + ->bind('topic', $topic->getId()) + ->bind('forum', $topic->getCategoryId()) + ->execute(); + } + + private static function memoizer() { + static $memoizer = null; + if($memoizer === null) + $memoizer = new Memoizer; + return $memoizer; + } + + private static function byQueryBase(): string { + return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE)); + } + public static function byTopicAndUser(ForumTopic $topic, User $user): ForumTopicTrack { + return self::memoizer()->find(function($track) use ($topic, $user) { + return $track->getTopicId() === $topic->getId() && $track->getUserId() === $user->getId(); + }, function() use ($topic, $user) { + $obj = DB::prepare(self::byQueryBase() . ' WHERE `topic_id` = :topic AND `user_id` = :user') + ->bind('topic', $topic->getId()) + ->bind('user', $user->getId()) + ->fetchObject(self::class); + if(!$obj) + throw new ForumTopicTrackNotFoundException; + return $obj; + }); + } + public static function byCategoryAndUser(ForumCategory $category, User $user): ForumTopicTrack { + return self::memoizer()->find(function($track) use ($category, $user) { + return $track->getCategoryId() === $category->getId() && $track->getUserId() === $user->getId(); + }, function() use ($category, $user) { + $obj = DB::prepare(self::byQueryBase() . ' WHERE `forum_id` = :category AND `user_id` = :user') + ->bind('category', $category->getId()) + ->bind('user', $user->getId()) + ->fetchObject(self::class); + if(!$obj) + throw new ForumTopicTrackNotFoundException; + return $obj; + }); + } } diff --git a/src/Forum/forum.php b/src/Forum/forum.php deleted file mode 100644 index 9d99be16..00000000 --- a/src/Forum/forum.php +++ /dev/null @@ -1,147 +0,0 @@ -<?php -function forum_get_parent_id(int $forumId): int { - if($forumId < 1) { - return 0; - } - - static $memoized = []; - - if(array_key_exists($forumId, $memoized)) { - return $memoized[$forumId]; - } - - $getParent = \Misuzu\DB::prepare(' - SELECT `forum_parent` - FROM `msz_forum_categories` - WHERE `forum_id` = :forum_id - '); - $getParent->bind('forum_id', $forumId); - - return (int)$getParent->fetchColumn(); -} - -function forum_get_child_ids(int $forumId): array { - if($forumId < 1) { - return []; - } - - static $memoized = []; - - if(array_key_exists($forumId, $memoized)) { - return $memoized[$forumId]; - } - - $getChildren = \Misuzu\DB::prepare(' - SELECT `forum_id` - FROM `msz_forum_categories` - WHERE `forum_parent` = :forum_id - '); - $getChildren->bind('forum_id', $forumId); - $children = $getChildren->fetchAll(); - - return $memoized[$forumId] = array_column($children, 'forum_id'); -} - -function forum_topics_unread(int $forumId, int $userId): int { - if($userId < 1 || $forumId < 1) { - return false; - } - - static $memoized = []; - $memoId = "{$forumId}-{$userId}"; - - if(array_key_exists($memoId, $memoized)) { - return $memoized[$memoId]; - } - - $memoized[$memoId] = 0; - $children = forum_get_child_ids($forumId); - - foreach($children as $child) { - $memoized[$memoId] += forum_topics_unread($child, $userId); - } - - if(forum_perms_check_user(MSZ_FORUM_PERMS_GENERAL, $forumId, $userId, MSZ_FORUM_PERM_SET_READ)) { - $countUnread = \Misuzu\DB::prepare(' - SELECT COUNT(ti.`topic_id`) - FROM `msz_forum_topics` AS ti - LEFT JOIN `msz_forum_topics_track` AS tt - ON tt.`topic_id` = ti.`topic_id` AND tt.`user_id` = :user_id - WHERE ti.`forum_id` = :forum_id - AND ti.`topic_deleted` IS NULL - AND ti.`topic_bumped` >= NOW() - INTERVAL 1 MONTH - AND ( - tt.`track_last_read` IS NULL - OR tt.`track_last_read` < ti.`topic_bumped` - ) - '); - $countUnread->bind('forum_id', $forumId); - $countUnread->bind('user_id', $userId); - $memoized[$memoId] += (int)$countUnread->fetchColumn(); - } - - return $memoized[$memoId]; -} - -function forum_timeout(int $forumId, int $userId): int { - $checkTimeout = \Misuzu\DB::prepare(' - SELECT TIMESTAMPDIFF(SECOND, COALESCE(MAX(`post_created`), NOW() - INTERVAL 1 YEAR), NOW()) - FROM `msz_forum_posts` - WHERE `forum_id` = :forum_id - AND `user_id` = :user_id - '); - $checkTimeout->bind('forum_id', $forumId); - $checkTimeout->bind('user_id', $userId); - - return (int)$checkTimeout->fetchColumn(); -} - -// $forumId == null marks all forums as read -function forum_mark_read(?int $forumId, int $userId): void { - if(($forumId !== null && $forumId < 1) || $userId < 1) { - return; - } - - $entireForum = $forumId === null; - - if(!$entireForum) { - $children = forum_get_child_ids($forumId); - - foreach($children as $child) { - forum_mark_read($child, $userId); - } - } - - $doMark = \Misuzu\DB::prepare(sprintf( - ' - INSERT INTO `msz_forum_topics_track` - (`user_id`, `topic_id`, `forum_id`, `track_last_read`) - SELECT u.`user_id`, t.`topic_id`, t.`forum_id`, NOW() - FROM `msz_forum_topics` AS t - LEFT JOIN `msz_users` AS u - ON u.`user_id` = :user - WHERE t.`topic_deleted` IS NULL - AND t.`topic_bumped` >= NOW() - INTERVAL 1 MONTH - %1$s - GROUP BY t.`topic_id` - ON DUPLICATE KEY UPDATE - `track_last_read` = NOW() - ', - $entireForum ? '' : 'AND t.`forum_id` = :forum' - )); - $doMark->bind('user', $userId); - - if(!$entireForum) { - $doMark->bind('forum', $forumId); - } - - $doMark->execute(); -} - -function forum_count_synchronise(int $forumId = \Misuzu\Forum\ForumCategory::ROOT_ID, bool $save = true): array { - try { - return \Misuzu\Forum\ForumCategory::byId($forumId)->synchronise($save); - } catch(\Misuzu\Forum\ForumCategoryNotFoundException $ex) { - return ['topics' => 0, 'posts' => 0]; - } -} diff --git a/src/Forum/perms.php b/src/Forum/perms.php index c8ddd9cc..acb45ce4 100644 --- a/src/Forum/perms.php +++ b/src/Forum/perms.php @@ -43,6 +43,27 @@ define('MSZ_FORUM_PERM_MODES', [ MSZ_FORUM_PERMS_GENERAL, ]); +function forum_get_parent_id(int $forumId): int { + if($forumId < 1) { + return 0; + } + + static $memoized = []; + + if(array_key_exists($forumId, $memoized)) { + return $memoized[$forumId]; + } + + $getParent = \Misuzu\DB::prepare(' + SELECT `forum_parent` + FROM `msz_forum_categories` + WHERE `forum_id` = :forum_id + '); + $getParent->bind('forum_id', $forumId); + + return (int)$getParent->fetchColumn(); +} + function forum_perms_get_user(?int $forum, int $user): array { $perms = perms_get_blank(MSZ_FORUM_PERM_MODES); diff --git a/src/Forum/post.php b/src/Forum/post.php deleted file mode 100644 index 34908660..00000000 --- a/src/Forum/post.php +++ /dev/null @@ -1,358 +0,0 @@ -<?php -function forum_post_create( - int $topicId, - int $forumId, - int $userId, - string $ipAddress, - string $text, - int $parser = \Misuzu\Parsers\Parser::PLAIN, - bool $displaySignature = true -): int { - $createPost = \Misuzu\DB::prepare(' - INSERT INTO `msz_forum_posts` - (`topic_id`, `forum_id`, `user_id`, `post_ip`, `post_text`, `post_parse`, `post_display_signature`) - VALUES - (:topic_id, :forum_id, :user_id, INET6_ATON(:post_ip), :post_text, :post_parse, :post_display_signature) - '); - $createPost->bind('topic_id', $topicId); - $createPost->bind('forum_id', $forumId); - $createPost->bind('user_id', $userId); - $createPost->bind('post_ip', $ipAddress); - $createPost->bind('post_text', $text); - $createPost->bind('post_parse', $parser); - $createPost->bind('post_display_signature', $displaySignature ? 1 : 0); - - return $createPost->execute() ? \Misuzu\DB::lastId() : 0; -} - -function forum_post_update( - int $postId, - string $ipAddress, - string $text, - int $parser = \Misuzu\Parsers\Parser::PLAIN, - bool $displaySignature = true, - bool $bumpUpdate = true -): bool { - if($postId < 1) { - return false; - } - - $updatePost = \Misuzu\DB::prepare(' - UPDATE `msz_forum_posts` - SET `post_ip` = INET6_ATON(:post_ip), - `post_text` = :post_text, - `post_parse` = :post_parse, - `post_display_signature` = :post_display_signature, - `post_edited` = IF(:bump, NOW(), `post_edited`) - WHERE `post_id` = :post_id - '); - $updatePost->bind('post_id', $postId); - $updatePost->bind('post_ip', $ipAddress); - $updatePost->bind('post_text', $text); - $updatePost->bind('post_parse', $parser); - $updatePost->bind('post_display_signature', $displaySignature ? 1 : 0); - $updatePost->bind('bump', $bumpUpdate ? 1 : 0); - - return $updatePost->execute(); -} - -function forum_post_find(int $postId, int $userId): array { - $getPostInfo = \Misuzu\DB::prepare(sprintf( - ' - SELECT - p.`post_id`, p.`topic_id`, - ( - SELECT COUNT(`post_id`) - FROM `msz_forum_posts` - WHERE `topic_id` = p.`topic_id` - AND `post_id` < p.`post_id` - AND `post_deleted` IS NULL - ORDER BY `post_id` - ) as `preceeding_post_count`, - ( - SELECT COUNT(`post_id`) - FROM `msz_forum_posts` - WHERE `topic_id` = p.`topic_id` - AND `post_id` < p.`post_id` - AND `post_deleted` IS NOT NULL - ORDER BY `post_id` - ) as `preceeding_post_deleted_count` - FROM `msz_forum_posts` AS p - WHERE p.`post_id` = :post_id - ')); - $getPostInfo->bind('post_id', $postId); - return $getPostInfo->fetch(); -} - -function forum_post_get(int $postId, bool $allowDeleted = false): array { - $getPost = \Misuzu\DB::prepare(sprintf( - ' - SELECT - p.`post_id`, p.`post_text`, p.`post_created`, p.`post_parse`, p.`post_display_signature`, - p.`topic_id`, p.`post_deleted`, p.`post_edited`, p.`topic_id`, p.`forum_id`, - INET6_NTOA(p.`post_ip`) AS `post_ip`, - u.`user_id` AS `poster_id`, u.`username` AS `poster_name`, - u.`user_created` AS `poster_joined`, u.`user_country` AS `poster_country`, - COALESCE(u.`user_colour`, r.`role_colour`) AS `poster_colour`, - ( - SELECT COUNT(`post_id`) - FROM `msz_forum_posts` - WHERE `user_id` = p.`user_id` - AND `post_deleted` IS NULL - ) AS `poster_post_count`, - ( - SELECT MIN(`post_id`) = p.`post_id` - FROM `msz_forum_posts` - WHERE `topic_id` = p.`topic_id` - ) AS `is_opening_post`, - ( - SELECT `user_id` = u.`user_id` - FROM `msz_forum_posts` - WHERE `topic_id` = p.`topic_id` - ORDER BY `post_id` - LIMIT 1 - ) AS `is_original_poster` - FROM `msz_forum_posts` AS p - LEFT JOIN `msz_users` AS u - ON u.`user_id` = p.`user_id` - LEFT JOIN `msz_roles` AS r - ON r.`role_id` = u.`display_role` - WHERE `post_id` = :post_id - %1$s - ORDER BY `post_id` - ', - $allowDeleted ? '' : 'AND `post_deleted` IS NULL' - )); - $getPost->bind('post_id', $postId); - return $getPost->fetch(); -} - -function forum_post_search(string $query): array { - $searchPosts = \Misuzu\DB::prepare(' - SELECT - p.`post_id`, p.`post_text`, p.`post_created`, p.`post_parse`, p.`post_display_signature`, - p.`topic_id`, p.`post_deleted`, p.`post_edited`, p.`topic_id`, p.`forum_id`, - INET6_NTOA(p.`post_ip`) AS `post_ip`, - u.`user_id` AS `poster_id`, u.`username` AS `poster_name`, - u.`user_created` AS `poster_joined`, u.`user_country` AS `poster_country`, - u.`user_signature_content` AS `poster_signature_content`, u.`user_signature_parser` AS `poster_signature_parser`, - COALESCE(u.`user_colour`, r.`role_colour`) AS `poster_colour`, - COALESCE(u.`user_title`, r.`role_title`) AS `poster_title`, - ( - SELECT COUNT(`post_id`) - FROM `msz_forum_posts` - WHERE `user_id` = p.`user_id` - AND `post_deleted` IS NULL - ) AS `poster_post_count`, - ( - SELECT MIN(`post_id`) = p.`post_id` - FROM `msz_forum_posts` - WHERE `topic_id` = p.`topic_id` - ) AS `is_opening_post`, - ( - SELECT `user_id` = u.`user_id` - FROM `msz_forum_posts` - WHERE `topic_id` = p.`topic_id` - ORDER BY `post_id` - LIMIT 1 - ) AS `is_original_poster` - FROM `msz_forum_posts` AS p - LEFT JOIN `msz_users` AS u - ON u.`user_id` = p.`user_id` - LEFT JOIN `msz_roles` AS r - ON r.`role_id` = u.`display_role` - WHERE MATCH(p.`post_text`) - AGAINST (:query IN NATURAL LANGUAGE MODE) - AND `post_deleted` IS NULL - ORDER BY `post_id` - '); - $searchPosts->bind('query', $query); - return $searchPosts->fetchAll(); -} - -function forum_post_count_user(int $userId, bool $showDeleted = false): int { - $getPosts = \Misuzu\DB::prepare(sprintf( - ' - SELECT COUNT(p.`post_id`) - FROM `msz_forum_posts` AS p - WHERE `user_id` = :user_id - %1$s - ', - $showDeleted ? '' : 'AND `post_deleted` IS NULL' - )); - $getPosts->bind('user_id', $userId); - - return (int)$getPosts->fetchColumn(); -} - -function forum_post_listing( - int $topicId, - int $offset = 0, - int $take = 0, - bool $showDeleted = false, - bool $selectAuthor = false -): array { - $hasPagination = $offset >= 0 && $take > 0; - $getPosts = \Misuzu\DB::prepare(sprintf( - ' - SELECT - p.`post_id`, p.`post_text`, p.`post_created`, p.`post_parse`, - p.`topic_id`, p.`post_deleted`, p.`post_edited`, p.`post_display_signature`, - INET6_NTOA(p.`post_ip`) AS `post_ip`, - u.`user_id` AS `poster_id`, u.`username` AS `poster_name`, - u.`user_created` AS `poster_joined`, u.`user_country` AS `poster_country`, - u.`user_signature_content` AS `poster_signature_content`, u.`user_signature_parser` AS `poster_signature_parser`, - COALESCE(u.`user_colour`, r.`role_colour`) AS `poster_colour`, - COALESCE(u.`user_title`, r.`role_title`) AS `poster_title`, - ( - SELECT COUNT(`post_id`) - FROM `msz_forum_posts` - WHERE `user_id` = p.`user_id` - AND `post_deleted` IS NULL - ) AS `poster_post_count`, - ( - SELECT MIN(`post_id`) = p.`post_id` - FROM `msz_forum_posts` - WHERE `topic_id` = p.`topic_id` - ) AS `is_opening_post`, - ( - SELECT `user_id` = u.`user_id` - FROM `msz_forum_posts` - WHERE `topic_id` = p.`topic_id` - ORDER BY `post_id` - LIMIT 1 - ) AS `is_original_poster` - FROM `msz_forum_posts` AS p - LEFT JOIN `msz_users` AS u - ON u.`user_id` = p.`user_id` - LEFT JOIN `msz_roles` AS r - ON r.`role_id` = u.`display_role` - WHERE %3$s = :topic_id - %1$s - ORDER BY `post_id` - %2$s - ', - $showDeleted ? '' : 'AND `post_deleted` IS NULL', - $hasPagination ? 'LIMIT :offset, :take' : '', - $selectAuthor ? 'p.`user_id`' : 'p.`topic_id`' - )); - $getPosts->bind('topic_id', $topicId); - - if($hasPagination) { - $getPosts->bind('offset', $offset); - $getPosts->bind('take', $take); - } - - return $getPosts->fetchAll(); -} - -define('MSZ_E_FORUM_POST_DELETE_OK', 0); // deleting is fine -define('MSZ_E_FORUM_POST_DELETE_USER', 1); // invalid user -define('MSZ_E_FORUM_POST_DELETE_POST', 2); // post doesn't exist -define('MSZ_E_FORUM_POST_DELETE_DELETED', 3); // post is already marked as deleted -define('MSZ_E_FORUM_POST_DELETE_OWNER', 4); // you may only delete your own posts -define('MSZ_E_FORUM_POST_DELETE_OLD', 5); // posts has existed for too long to be deleted -define('MSZ_E_FORUM_POST_DELETE_PERM', 6); // you aren't allowed to delete posts -define('MSZ_E_FORUM_POST_DELETE_OP', 7); // this is the opening post of a topic - -// only allow posts made within a week of posting to be deleted by normal users -define('MSZ_FORUM_POST_DELETE_LIMIT', 60 * 60 * 24 * 7); - -// set $userId to null for system request, make sure this is NEVER EVER null on user request -// $postId can also be a the return value of forum_post_get if you already grabbed it once before -function forum_post_can_delete($postId, ?int $userId = null): int { - if($userId !== null && $userId < 1) { - return MSZ_E_FORUM_POST_DELETE_USER; - } - - if(is_array($postId)) { - $post = $postId; - } else { - $post = forum_post_get((int)$postId, true); - } - - if(empty($post)) { - return MSZ_E_FORUM_POST_DELETE_POST; - } - - $isSystemReq = $userId === null; - $perms = $isSystemReq ? 0 : forum_perms_get_user($post['forum_id'], $userId)[MSZ_FORUM_PERMS_GENERAL]; - $canDeleteAny = $isSystemReq ? true : perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST); - $canViewPost = $isSystemReq ? true : perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM); - $postIsDeleted = !empty($post['post_deleted']); - - if(!$canViewPost) { - return MSZ_E_FORUM_POST_DELETE_POST; - } - - if($post['is_opening_post']) { - return MSZ_E_FORUM_POST_DELETE_OP; - } - - if($postIsDeleted) { - return $canDeleteAny ? MSZ_E_FORUM_POST_DELETE_DELETED : MSZ_E_FORUM_POST_DELETE_POST; - } - - if($isSystemReq) { - return MSZ_E_FORUM_POST_DELETE_OK; - } - - if(!$canDeleteAny) { - if(!perms_check($perms, MSZ_FORUM_PERM_DELETE_POST)) { - return MSZ_E_FORUM_POST_DELETE_PERM; - } - - if($post['poster_id'] !== $userId) { - return MSZ_E_FORUM_POST_DELETE_OWNER; - } - - if(strtotime($post['post_created']) <= time() - MSZ_FORUM_POST_DELETE_LIMIT) { - return MSZ_E_FORUM_POST_DELETE_OLD; - } - } - - return MSZ_E_FORUM_POST_DELETE_OK; -} - -function forum_post_delete(int $postId): bool { - if($postId < 1) { - return false; - } - - $markDeleted = \Misuzu\DB::prepare(' - UPDATE `msz_forum_posts` - SET `post_deleted` = NOW() - WHERE `post_id` = :post - AND `post_deleted` IS NULL - '); - $markDeleted->bind('post', $postId); - return $markDeleted->execute(); -} - -function forum_post_restore(int $postId): bool { - if($postId < 1) { - return false; - } - - $markDeleted = \Misuzu\DB::prepare(' - UPDATE `msz_forum_posts` - SET `post_deleted` = NULL - WHERE `post_id` = :post - AND `post_deleted` IS NOT NULL - '); - $markDeleted->bind('post', $postId); - return $markDeleted->execute(); -} - -function forum_post_nuke(int $postId): bool { - if($postId < 1) { - return false; - } - - $markDeleted = \Misuzu\DB::prepare(' - DELETE FROM `msz_forum_posts` - WHERE `post_id` = :post - '); - $markDeleted->bind('post', $postId); - return $markDeleted->execute(); -} diff --git a/src/Forum/topic.php b/src/Forum/topic.php deleted file mode 100644 index b9d5cdf4..00000000 --- a/src/Forum/topic.php +++ /dev/null @@ -1,56 +0,0 @@ -<?php -function forum_topic_views_increment(int $topicId): void { - if($topicId < 1) { - return; - } - - $bumpViews = \Misuzu\DB::prepare(' - UPDATE `msz_forum_topics` - SET `topic_count_views` = `topic_count_views` + 1 - WHERE `topic_id` = :topic_id - '); - $bumpViews->bind('topic_id', $topicId); - $bumpViews->execute(); -} - -function forum_topic_mark_read(int $userId, int $topicId, int $forumId): void { - if($userId < 1) { - return; - } - - // previously a TRIGGER was used to achieve this behaviour, - // but those explode when running on a lot of queries (like forum_mark_read() does) - // so instead we get to live with this garbage now - // JUST TO CLARIFY: "this behaviour" refers to forum_topic_views_increment only being executed when the topic is viewed for the first time - try { - $markAsRead = \Misuzu\DB::prepare(' - INSERT INTO `msz_forum_topics_track` - (`user_id`, `topic_id`, `forum_id`, `track_last_read`) - VALUES - (:user_id, :topic_id, :forum_id, NOW()) - '); - $markAsRead->bind('user_id', $userId); - $markAsRead->bind('topic_id', $topicId); - $markAsRead->bind('forum_id', $forumId); - - if($markAsRead->execute()) { - forum_topic_views_increment($topicId); - } - } catch(PDOException $ex) { - if($ex->getCode() != '23000') { - throw $ex; - } - - $markAsRead = \Misuzu\DB::prepare(' - UPDATE `msz_forum_topics_track` - SET `track_last_read` = NOW(), - `forum_id` = :forum_id - WHERE `user_id` = :user_id - AND `topic_id` = :topic_id - '); - $markAsRead->bind('user_id', $userId); - $markAsRead->bind('topic_id', $topicId); - $markAsRead->bind('forum_id', $forumId); - $markAsRead->execute(); - } -} diff --git a/src/Users/User.php b/src/Users/User.php index dc99d276..1caacf4f 100644 --- a/src/Users/User.php +++ b/src/Users/User.php @@ -10,6 +10,7 @@ use Misuzu\HasRankInterface; use Misuzu\Memoizer; use Misuzu\Pagination; use Misuzu\TOTP; +use Misuzu\Forum\ForumCategory; use Misuzu\Net\IPAddress; use Misuzu\Parsers\Parser; use Misuzu\Users\Assets\UserAvatarAsset; diff --git a/templates/forum/macros.twig b/templates/forum/macros.twig index 6ef1b27f..25d05cdf 100644 --- a/templates/forum/macros.twig +++ b/templates/forum/macros.twig @@ -156,7 +156,7 @@ <a href="{{ url('forum-category', {'forum': category.id}) }}" class="forum__category__link"></a> <div class="forum__category__container"> - <div class="forum__category__icon forum__category__icon--{{ category.unread(user) ? 'un' : '' }}read"> + <div class="forum__category__icon forum__category__icon--{{ user is null or category.hasRead(user) ? '' : 'un' }}read"> <span class="{{ category.icon }}"></span> </div> @@ -176,7 +176,7 @@ {% for child in category.children %} {% if child.canView(user) %} <a href="{{ url('forum-category', {'forum': child.id}) }}" - class="forum__category__subforum{% if child.unread(user) %} forum__category__subforum--unread{% endif %}"> + class="forum__category__subforum{% if user is not null and not child.hasRead(user) %} forum__category__subforum--unread{% endif %}"> {{ child.name }} </a> {% endif %} @@ -401,7 +401,7 @@ <a href="{{ url('forum-topic', {'topic': topic.id}) }}" class="forum__topic__link"></a> <div class="forum__topic__container"> - <div class="forum__topic__icon forum__topic__icon--{{ topic.hasUnread(user) ? 'un' : '' }}read{% if topic.hasPriorityVoting %} forum__topic__icon--wide{% endif %}"> + <div class="forum__topic__icon forum__topic__icon--{{ user is null or topic.hasRead(user) ? '' : 'un' }}read{% if topic.hasPriorityVoting %} forum__topic__icon--wide{% endif %}"> <i class="{{ topic.icon(user) }}{% if topic.hasPriorityVoting %} forum__topic__icon--faded{% endif %}"></i> {% if topic.hasPriorityVoting %} @@ -727,7 +727,7 @@ perms|perms_check(constant('MSZ_FORUM_PERM_DELETE_ANY_POST')) or ( user_id == post.poster_id and perms|perms_check(constant('MSZ_FORUM_PERM_DELETE_POST')) - and post.post_created|date('U') > ''|date('U') - constant('MSZ_FORUM_POST_DELETE_LIMIT') + and post.post_created|date('U') > ''|date('U') - constant('\\Misuzu\\Forum\\ForumPost::DELETE_AGE_LIMIT') ) ) %} diff --git a/templates/forum/posting.twig b/templates/forum/posting.twig index 3925d337..7a1b0f70 100644 --- a/templates/forum/posting.twig +++ b/templates/forum/posting.twig @@ -5,7 +5,7 @@ {% set title = 'Posting' %} {% set is_reply = posting_topic is defined %} -{% set is_opening = not is_reply or posting_post.is_opening_post|default(false) %} +{% set is_opening = not is_reply or posting_post.isOpeningPost|default(false) %} {% block content %} <form method="post" action="{{ url('forum-' ~ (is_reply ? 'post' : 'topic') ~ '-create') }}"> @@ -32,7 +32,7 @@ ) }} {% if posting_post is defined %} - {{ input_hidden('post[id]', posting_post.post_id) }} + {{ input_hidden('post[id]', posting_post.id) }} {% endif %} {% if posting_notices|length > 0 %} @@ -45,21 +45,21 @@ </div> {% endif %} - <div class="container forum__post js-forum-posting" style="{{ posting_post.poster_colour|default(current_user.colour.raw)|html_colour('--accent-colour') }}"> + <div class="container forum__post js-forum-posting" style="{{ posting_post.user.colour.raw|default(current_user.colour.raw)|html_colour('--accent-colour') }}"> <div class="forum__post__info"> <div class="forum__post__info__background"></div> <div class="forum__post__info__content"> - <span class="forum__post__avatar">{{ avatar(posting_post.poster_id|default(current_user.id), 120, posting_post.poster_name|default(current_user.username)) }}</span> + <span class="forum__post__avatar">{{ avatar(posting_post.user.id|default(current_user.id), 120, posting_post.user.username|default(current_user.username)) }}</span> - <span class="forum__post__username">{{ posting_post.poster_name|default(current_user.username) }}</span> + <span class="forum__post__username">{{ posting_post.user.username|default(current_user.username) }}</span> <div class="forum__post__icons"> - <div class="flag flag--{{ posting_post.poster_country|default(posting_user.country)|lower }}" title="{{ posting_post.poster_country|default(posting_user.country)|country_name }}"></div> - <div class="forum__post__posts-count">{{ posting_post.poster_post_count|default(posting_user.forumPostCount)|number_format }} posts</div> + <div class="flag flag--{{ posting_post.user.country|default(posting_user.country)|lower }}" title="{{ posting_post.user.country|default(posting_user.country)|country_name }}"></div> + <div class="forum__post__posts-count">{{ posting_post.user.forumPostCount|default(posting_user.forumPostCount)|number_format }} posts</div> </div> <div class="forum__post__joined"> - joined <time datetime="{{ posting_post.poster_joined|default(posting_user.createdTime)|date('c') }}" title="{{ posting_post.poster_joined|default(posting_user.createdTime)|date('r') }}">{{ posting_post.poster_joined|default(posting_user.createdTime)|time_diff }}</time> + joined <time datetime="{{ posting_post.user.createdTime|default(posting_user.createdTime)|date('c') }}" title="{{ posting_post.user.createdTime|default(posting_user.createdTime)|date('r') }}">{{ posting_post.user.createdTime|default(posting_user.createdTime)|time_diff }}</time> </div> </div> </div> @@ -77,7 +77,7 @@ </span> </div> - <textarea name="post[text]" class="forum__post__text forum__post__text--edit js-forum-posting-text js-ctrl-enter-submit" placeholder="Type your post content here...">{{ posting_defaults.text|default(posting_post.post_text|default('')) }}</textarea> + <textarea name="post[text]" class="forum__post__text forum__post__text--edit js-forum-posting-text js-ctrl-enter-submit" placeholder="Type your post content here...">{{ posting_defaults.text|default(posting_post.body|default('')) }}</textarea> <div class="forum__post__text js-forum-posting-preview" hidden></div> <div class="forum__post__actions forum__post__actions--bbcode" hidden> @@ -142,7 +142,7 @@ {{ input_select( 'post[parser]', constant('\\Misuzu\\Parsers\\Parser::NAMES'), - posting_defaults.parser|default(posting_post.post_parse|default(posting_user.preferredParser)), + posting_defaults.parser|default(posting_post.bodyParser|default(posting_user.preferredParser)), null, null, false, 'forum__post__dropdown js-forum-posting-parser' ) }} {% if is_opening and posting_types|length > 1 %} @@ -158,8 +158,8 @@ 'Display Signature', posting_defaults.signature is not null ? posting_defaults.signature : ( - posting_post.post_display_signature is defined - ? posting_post.post_display_signature + posting_post.shouldDisplaySignature is defined + ? posting_post.shouldDisplaySignature : true ) ) }}