From 39c6269cf38064d4c1bc961dfd1ee20baff73348 Mon Sep 17 00:00:00 2001 From: flashwave Date: Mon, 28 Aug 2023 01:17:34 +0000 Subject: [PATCH] Rewrote forum backend. --- composer.lock | 24 +- misuzu.php | 6 - public-legacy/forum/forum.php | 206 ++- public-legacy/forum/index.php | 226 +++- public-legacy/forum/leaderboard.php | 123 +- public-legacy/forum/post.php | 163 ++- public-legacy/forum/posting.php | 288 +++-- public-legacy/forum/topic.php | 320 +++-- public-legacy/manage/forum/category.php | 22 - public-legacy/manage/forum/index.php | 3 +- public-legacy/manage/forum/redirs.php | 17 +- public-legacy/members.php | 5 +- public-legacy/profile.php | 210 ++- public-legacy/search.php | 217 +++- src/Forum/Forum.php | 1558 +++++++++++++++++++++++ src/Forum/ForumCategoryInfo.php | 194 +++ src/Forum/ForumPostInfo.php | 135 ++ src/Forum/ForumTopicInfo.php | 170 +++ src/Forum/ForumTopicRedirectInfo.php | 43 + src/Forum/forum.php | 586 --------- src/Forum/leaderboard.php | 98 -- src/Forum/perms.php | 212 --- src/Forum/post.php | 363 ------ src/Forum/topic.php | 719 ----------- src/Forum/validate.php | 33 - src/MisuzuContext.php | 37 +- src/Template.php | 5 + src/TwigMisuzu.php | 8 +- src/Users/Assets/AssetsRoutes.php | 13 +- src/Users/UserInfo.php | 25 + src/Users/Users.php | 1 + src/manage.php | 2 +- src/perms.php | 107 +- src/url.php | 3 +- templates/forum/forum.twig | 14 +- templates/forum/index.twig | 14 +- templates/forum/leaderboard.twig | 26 +- templates/forum/macros.twig | 406 +++--- templates/forum/posting.twig | 43 +- templates/forum/topic.twig | 24 +- templates/home/search.twig | 21 +- templates/manage/forum/forum.twig | 11 - templates/manage/forum/listing.twig | 12 +- templates/manage/forum/redirs.twig | 12 +- templates/profile/index.twig | 54 +- templates/profile/posts.twig | 23 - templates/profile/topics.twig | 23 - tools/cron | 5 +- 48 files changed, 3798 insertions(+), 3032 deletions(-) delete mode 100644 public-legacy/manage/forum/category.php create mode 100644 src/Forum/Forum.php create mode 100644 src/Forum/ForumCategoryInfo.php create mode 100644 src/Forum/ForumPostInfo.php create mode 100644 src/Forum/ForumTopicInfo.php create mode 100644 src/Forum/ForumTopicRedirectInfo.php delete mode 100644 src/Forum/forum.php delete mode 100644 src/Forum/leaderboard.php delete mode 100644 src/Forum/perms.php delete mode 100644 src/Forum/post.php delete mode 100644 src/Forum/topic.php delete mode 100644 src/Forum/validate.php delete mode 100644 templates/manage/forum/forum.twig delete mode 100644 templates/profile/posts.twig delete mode 100644 templates/profile/topics.twig diff --git a/composer.lock b/composer.lock index 5930416..eaea418 100644 --- a/composer.lock +++ b/composer.lock @@ -348,7 +348,7 @@ "source": { "type": "git", "url": "https://git.flash.moe/flash/index.git", - "reference": "553b7c4a14aa7f2403c87ce474933986ac17d040" + "reference": "6a38f803f4b3e49296f7472743e7c683c496ec19" }, "require": { "ext-mbstring": "*", @@ -386,20 +386,20 @@ ], "description": "Composer package for the common library for my projects.", "homepage": "https://railgun.sh/index", - "time": "2023-08-03T01:29:57+00:00" + "time": "2023-08-22T00:04:20+00:00" }, { "name": "matomo/device-detector", - "version": "6.1.4", + "version": "6.1.5", "source": { "type": "git", "url": "https://github.com/matomo-org/device-detector.git", - "reference": "74f6c4f6732b3ad6cdf25560746841d522969112" + "reference": "40ca2990dba2c1719e5c62168e822e0b86c167d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/74f6c4f6732b3ad6cdf25560746841d522969112", - "reference": "74f6c4f6732b3ad6cdf25560746841d522969112", + "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/40ca2990dba2c1719e5c62168e822e0b86c167d4", + "reference": "40ca2990dba2c1719e5c62168e822e0b86c167d4", "shasum": "" }, "require": { @@ -455,7 +455,7 @@ "source": "https://github.com/matomo-org/matomo", "wiki": "https://dev.matomo.org/" }, - "time": "2023-08-02T08:48:53+00:00" + "time": "2023-08-17T16:17:41+00:00" }, { "name": "mustangostang/spyc", @@ -1616,16 +1616,16 @@ "packages-dev": [ { "name": "phpstan/phpstan", - "version": "1.10.26", + "version": "1.10.32", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "5d660cbb7e1b89253a47147ae44044f49832351f" + "reference": "c47e47d3ab03137c0e121e77c4d2cb58672f6d44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/5d660cbb7e1b89253a47147ae44044f49832351f", - "reference": "5d660cbb7e1b89253a47147ae44044f49832351f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c47e47d3ab03137c0e121e77c4d2cb58672f6d44", + "reference": "c47e47d3ab03137c0e121e77c4d2cb58672f6d44", "shasum": "" }, "require": { @@ -1674,7 +1674,7 @@ "type": "tidelift" } ], - "time": "2023-07-19T12:44:37+00:00" + "time": "2023-08-24T21:54:50+00:00" } ], "aliases": [], diff --git a/misuzu.php b/misuzu.php index 73cdf48..85c568a 100644 --- a/misuzu.php +++ b/misuzu.php @@ -26,12 +26,6 @@ require_once MSZ_ROOT . '/utility.php'; require_once MSZ_SOURCE . '/perms.php'; require_once MSZ_SOURCE . '/manage.php'; require_once MSZ_SOURCE . '/url.php'; -require_once MSZ_SOURCE . '/Forum/perms.php'; -require_once MSZ_SOURCE . '/Forum/forum.php'; -require_once MSZ_SOURCE . '/Forum/leaderboard.php'; -require_once MSZ_SOURCE . '/Forum/post.php'; -require_once MSZ_SOURCE . '/Forum/topic.php'; -require_once MSZ_SOURCE . '/Forum/validate.php'; $dbConfig = parse_ini_file(MSZ_CONFIG . '/config.ini', true, INI_SCANNER_TYPED); diff --git a/public-legacy/forum/forum.php b/public-legacy/forum/forum.php index 11b0a36..9c397e0 100644 --- a/public-legacy/forum/forum.php +++ b/public-legacy/forum/forum.php @@ -1,77 +1,199 @@ getForum(); +$users = $msz->getUsers(); -$forum = forum_get($forumId); -$forumUser = $msz->getActiveUser(); -$forumUserId = $forumUser === null ? '0' : $forumUser->getId(); +$categoryId = (int)filter_input(INPUT_GET, 'f', FILTER_SANITIZE_NUMBER_INT); -if(empty($forum) || ($forum['forum_type'] == MSZ_FORUM_TYPE_LINK && empty($forum['forum_link']))) { +try { + $categoryInfo = $forum->getCategory(categoryId: $categoryId); +} catch(RuntimeException $ex) { echo render_error(404); return; } -$perms = forum_perms_get_user($forum['forum_id'], $forumUserId)[MSZ_FORUM_PERMS_GENERAL]; +$currentUser = $msz->getActiveUser(); +$currentUserId = $currentUser === null ? '0' : $currentUser->getId(); + +$perms = forum_perms_get_user($categoryInfo->getId(), $currentUserId)[MSZ_FORUM_PERMS_GENERAL]; if(!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM)) { echo render_error(403); return; } -if(isset($forumUser) && $msz->hasActiveBan($forumUser)) - $perms &= ~MSZ_FORUM_PERM_SET_WRITE; +if(isset($currentUser) && $msz->hasActiveBan($currentUser)) + $perms &= MSZ_FORUM_PERM_LIST_FORUM | MSZ_FORUM_PERM_VIEW_FORUM; -Template::set('forum_perms', $perms); - -if($forum['forum_type'] == MSZ_FORUM_TYPE_LINK) { - forum_increment_clicks($forum['forum_id']); - redirect($forum['forum_link']); +if($categoryInfo->isLink()) { + if($categoryInfo->hasLinkTarget()) { + $forum->incrementCategoryClicks($categoryInfo); + redirect($categoryInfo->getLinkTarget()); + } else render_error(404); return; } -$forumPagination = new Pagination($forum['forum_topic_count'], 20); +$forumPagination = new Pagination($forum->countTopics( + categoryInfo: $categoryInfo, + global: true, + deleted: perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST) ? null : false +), 20); -if(!$forumPagination->hasValidOffset() && $forum['forum_topic_count'] > 0) { +if(!$forumPagination->hasValidOffset()) { echo render_error(404); return; } -$forumMayHaveTopics = forum_may_have_topics($forum['forum_type']); -$topics = $forumMayHaveTopics - ? forum_topic_listing( - $forum['forum_id'], - $forumUserId, - $forumPagination->getOffset(), - $forumPagination->getRange(), - perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST) - ) - : []; +$userInfos = []; +$userColours = []; +$children = []; +$topics = []; -$forumMayHaveChildren = forum_may_have_children($forum['forum_type']); +if($categoryInfo->mayHaveChildren()) { + $children = $forum->getCategoryChildren($categoryInfo, hidden: false, asTree: true); -if($forumMayHaveChildren) { - $forum['forum_subforums'] = forum_get_children($forum['forum_id'], $forumUserId); + foreach($children as $child) { + $childPerms = forum_perms_get_user($child->info->getId(), (int)$currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + if(!perms_check($childPerms, MSZ_FORUM_PERM_LIST_FORUM)) { + unset($category->children[$childId]); + continue; + } - foreach($forum['forum_subforums'] as $skey => $subforum) { - $forum['forum_subforums'][$skey]['forum_subforums'] - = forum_get_children($subforum['forum_id'], $forumUserId); + $childUnread = false; + + if($child->info->mayHaveChildren()) { + foreach($child->children as $grandChildId => $grandChild) { + $grandChildPerms = forum_perms_get_user($grandChild->info->getId(), (int)$currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + if(!perms_check($grandChildPerms, MSZ_FORUM_PERM_LIST_FORUM)) { + unset($child->children[$grandChildId]); + continue; + } + + $grandChildUnread = false; + + if($grandChild->info->mayHaveTopics()) { + $catIds = [$grandChild->info->getId()]; + foreach($grandChild->childIds as $greatGrandChildId) { + $greatGrandChildPerms = forum_perms_get_user($greatGrandChildId, (int)$currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + if(perms_check($greatGrandChildPerms, MSZ_FORUM_PERM_LIST_FORUM)) + $catIds[] = $greatGrandChildId; + } + + $grandChildUnread = $forum->checkCategoryUnread($catIds, $currentUser); + if($grandChildUnread) + $childUnread = true; + } + + $grandChild->perms = $grandChildPerms; + $grandChild->unread = $grandChildUnread; + } + } + + if($child->info->mayHaveChildren() || $child->info->mayHaveTopics()) { + $catIds = [$child->info->getId()]; + foreach($child->childIds as $grandChildId) { + $grandChildPerms = forum_perms_get_user($grandChildId, (int)$currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + if(perms_check($grandChildPerms, MSZ_FORUM_PERM_LIST_FORUM)) + $catIds[] = $grandChildId; + } + + try { + $lastPostInfo = $forum->getPost(categoryInfos: $catIds, getLast: true, deleted: false); + } catch(RuntimeException $ex) { + $lastPostInfo = null; + } + + if($lastPostInfo !== null) { + $child->lastPost = new stdClass; + $child->lastPost->info = $lastPostInfo; + $child->lastPost->topicInfo = $forum->getTopic(postInfo: $lastPostInfo); + + if($lastPostInfo->hasUserId()) { + $lastPostUserId = $lastPostInfo->getUserId(); + if(!array_key_exists($lastPostUserId, $userInfos)) { + $userInfo = $users->getUser($lastPostUserId, 'id'); + $userInfos[$lastPostUserId] = $userInfo; + $userColours[$lastPostUserId] = $users->getUserColour($userInfo); + } + + $child->lastPost->user = $userInfos[$lastPostUserId]; + $child->lastPost->colour = $userColours[$lastPostUserId]; + } + } + } + + if($child->info->mayHaveTopics() && !$childUnread) + $childUnread = $forum->checkCategoryUnread($child->info, $currentUser); + + $child->perms = $childPerms; + $child->unread = $childUnread; } } +if($categoryInfo->mayHaveTopics()) { + $topicInfos = $forum->getTopics( + categoryInfo: $categoryInfo, + global: true, + deleted: perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST) ? null : false, + pagination: $forumPagination, + ); + + foreach($topicInfos as $topicInfo) { + $topics[] = $topic = new stdClass; + $topic->info = $topicInfo; + $topic->unread = $forum->checkTopicUnread($topicInfo, $currentUser); + $topic->participated = $forum->checkTopicParticipated($topicInfo, $currentUser); + $topic->lastPost = new stdClass; + + if($topicInfo->hasUserId()) { + $lastTopicUserId = $topicInfo->getUserId(); + if(!array_key_exists($lastTopicUserId, $userInfos)) { + $userInfo = $users->getUser($lastTopicUserId, 'id'); + $userInfos[$lastTopicUserId] = $userInfo; + $userColours[$lastTopicUserId] = $users->getUserColour($userInfo); + } + + $topic->user = $userInfos[$lastTopicUserId]; + $topic->colour = $userColours[$lastTopicUserId]; + } + + try { + $topic->lastPost->info = $lastPostInfo = $forum->getPost( + topicInfo: $topicInfo, + getLast: true, + deleted: $topicInfo->isDeleted() ? null : false, + ); + + if($lastPostInfo->hasUserId()) { + $lastPostUserId = $lastPostInfo->getUserId(); + if(!array_key_exists($lastPostUserId, $userInfos)) { + $userInfo = $users->getUser($lastPostUserId, 'id'); + $userInfos[$lastPostUserId] = $userInfo; + $userColours[$lastPostUserId] = $users->getUserColour($userInfo); + } + + $topic->lastPost->user = $userInfos[$lastPostUserId]; + $topic->lastPost->colour = $userColours[$lastPostUserId]; + } + } catch(RuntimeException $ex) {} + } +} + +$perms = perms_check_bulk($perms, [ + 'can_create_topic' => MSZ_FORUM_PERM_CREATE_TOPIC, +]); + Template::render('forum.forum', [ - 'forum_breadcrumbs' => forum_get_breadcrumbs($forum['forum_id']), - 'global_accent_colour' => forum_get_colour($forum['forum_id']), - 'forum_may_have_topics' => $forumMayHaveTopics, - 'forum_may_have_children' => $forumMayHaveChildren, - 'forum_info' => $forum, + 'forum_breadcrumbs' => $forum->getCategoryAncestry($categoryInfo), + 'global_accent_colour' => $forum->getCategoryColour($categoryInfo), + 'forum_info' => $categoryInfo, + 'forum_children' => $children, 'forum_topics' => $topics, 'forum_pagination' => $forumPagination, - 'forum_show_mark_as_read' => $forumUser !== null, + 'forum_show_mark_as_read' => $currentUser !== null, + 'forum_perms' => $perms, ]); diff --git a/public-legacy/forum/index.php b/public-legacy/forum/index.php index a95896b..2fcc64a 100644 --- a/public-legacy/forum/index.php +++ b/public-legacy/forum/index.php @@ -1,57 +1,207 @@ getForum(); +$users = $msz->getUsers(); +$mode = (string)filter_input(INPUT_GET, 'm'); $currentUser = $msz->getActiveUser(); $currentUserId = $currentUser === null ? '0' : $currentUser->getId(); -switch($indexMode) { - case 'mark': - if(!$msz->isLoggedIn()) { - echo render_error(403); - break; +if($mode === 'mark') { + if(!$msz->isLoggedIn()) { + echo render_error(403); + return; + } + + $categoryId = filter_input(INPUT_GET, 'f', FILTER_SANITIZE_NUMBER_INT); + + if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { + $categoryInfos = $categoryId === null + ? $forum->getCategories() + : $forum->getCategoryChildren(parentInfo: $categoryId, includeSelf: true); + + foreach($categoryInfos as $categoryInfo) { + $perms = forum_perms_get_user($categoryInfo->getId(), (int)$currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + if(perms_check($perms, MSZ_FORUM_PERM_LIST_FORUM)) + $forum->updateUserReadCategory($userInfo, $categoryInfo); } - if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { - forum_mark_read($forumId, (int)$msz->getAuthInfo()->getUserId()); - $redirect = url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]); - redirect($redirect); - break; - } + url_redirect($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]); + return; + } - Template::render('confirm', [ - 'title' => 'Mark forum as read', - 'message' => 'Are you sure you want to mark ' . ($forumId === 0 ? 'the entire' : 'this') . ' forum as read?', - 'return' => url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]), - 'params' => [ - 'forum' => $forumId, - ] - ]); - break; + Template::render('confirm', [ + 'title' => 'Mark forum as read', + 'message' => 'Are you sure you want to mark ' . ($categoryId < 1 ? 'the entire' : 'this') . ' forum as read?', + 'return' => url($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]), + 'params' => [ + 'forum' => $categoryId, + ] + ]); + return; +} - default: - $categories = forum_get_root_categories($currentUserId); - $blankForum = count($categories) < 1; +if($mode !== '') { + echo render_error(404); + return; +} - foreach($categories as $key => $category) { - $categories[$key]['forum_subforums'] = forum_get_children($category['forum_id'], $currentUserId); +$userInfos = []; +$userColours = []; +$categories = $forum->getCategories(hidden: false, asTree: true); - foreach($categories[$key]['forum_subforums'] as $skey => $sub) { - if(!forum_may_have_children($sub['forum_type'])) { - continue; +foreach($categories as $categoryId => $category) { + $perms = forum_perms_get_user($category->info->getId(), (int)$currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + if(!perms_check($perms, MSZ_FORUM_PERM_LIST_FORUM)) { + unset($categories[$categoryId]); + continue; + } + + $unread = false; + + if($category->info->mayHaveChildren()) + foreach($category->children as $childId => $child) { + $childPerms = forum_perms_get_user($child->info->getId(), (int)$currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + if(!perms_check($childPerms, MSZ_FORUM_PERM_LIST_FORUM)) { + unset($category->children[$childId]); + continue; + } + + $childUnread = false; + + if($category->info->isListing()) { + if($child->info->mayHaveChildren()) { + foreach($child->children as $grandChildId => $grandChild) { + $grandChildPerms = forum_perms_get_user($grandChild->info->getId(), (int)$currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + if(!perms_check($grandChildPerms, MSZ_FORUM_PERM_LIST_FORUM)) { + unset($child->children[$grandChildId]); + continue; + } + + $grandChildUnread = false; + + if($grandChild->info->mayHaveTopics()) { + $catIds = [$grandChild->info->getId()]; + foreach($grandChild->childIds as $greatGrandChildId) { + $greatGrandChildPerms = forum_perms_get_user($greatGrandChildId, (int)$currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + if(perms_check($greatGrandChildPerms, MSZ_FORUM_PERM_LIST_FORUM)) + $catIds[] = $greatGrandChildId; + } + + $grandChildUnread = $forum->checkCategoryUnread($catIds, $currentUser); + if($grandChildUnread) + $childUnread = true; + } + + $grandChild->perms = $grandChildPerms; + $grandChild->unread = $grandChildUnread; + } } - $categories[$key]['forum_subforums'][$skey]['forum_subforums'] - = forum_get_children($sub['forum_id'], $currentUserId); + if($child->info->mayHaveChildren() || $child->info->mayHaveTopics()) { + $catIds = [$child->info->getId()]; + foreach($child->childIds as $grandChildId) { + $grandChildPerms = forum_perms_get_user($grandChildId, (int)$currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + if(perms_check($grandChildPerms, MSZ_FORUM_PERM_LIST_FORUM)) + $catIds[] = $grandChildId; + } + + try { + $lastPostInfo = $forum->getPost(categoryInfos: $catIds, getLast: true, deleted: false); + } catch(RuntimeException $ex) { + $lastPostInfo = null; + } + + if($lastPostInfo !== null) { + $child->lastPost = new stdClass; + $child->lastPost->info = $lastPostInfo; + $child->lastPost->topicInfo = $forum->getTopic(postInfo: $lastPostInfo); + + if($lastPostInfo->hasUserId()) { + $lastPostUserId = $lastPostInfo->getUserId(); + if(!array_key_exists($lastPostUserId, $userInfos)) { + $userInfo = $users->getUser($lastPostUserId, 'id'); + $userInfos[$lastPostUserId] = $userInfo; + $userColours[$lastPostUserId] = $users->getUserColour($userInfo); + } + + $child->lastPost->user = $userInfos[$lastPostUserId]; + $child->lastPost->colour = $userColours[$lastPostUserId]; + } + } + } } + + if($child->info->mayHaveTopics() && !$childUnread) { + $childUnread = $forum->checkCategoryUnread($child->info, $currentUser); + if($childUnread) + $unread = true; + } + + $child->perms = $childPerms; + $child->unread = $childUnread; } - Template::render('forum.index', [ - 'forum_categories' => $categories, - 'forum_empty' => $blankForum, - 'forum_show_mark_as_read' => $currentUser !== null, - ]); - break; + if($category->info->mayHaveTopics() && !$unread) + $unread = $forum->checkCategoryUnread($category->info, $currentUser); + + if(!$category->info->isListing()) { + if(!array_key_exists('0', $categories)) { + $categories['0'] = $root = new stdClass; + $root->info = null; + $root->perms = 0; + $root->unread = false; + $root->colour = null; + $root->children = []; + } + + $categories['0']->children[$categoryId] = $category; + unset($categories[$categoryId]); + + if($category->info->mayHaveChildren() || $category->info->mayHaveTopics()) { + $catIds = [$category->info->getId()]; + foreach($category->childIds as $childId) { + $childPerms = forum_perms_get_user($childId, (int)$currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + if(perms_check($childPerms, MSZ_FORUM_PERM_LIST_FORUM)) + $catIds[] = $childId; + } + + try { + $lastPostInfo = $forum->getPost(categoryInfos: $catIds, getLast: true, deleted: false); + } catch(RuntimeException $ex) { + $lastPostInfo = null; + } + + if($lastPostInfo !== null) { + $category->lastPost = new stdClass; + $category->lastPost->info = $lastPostInfo; + $category->lastPost->topicInfo = $forum->getTopic(postInfo: $lastPostInfo); + + if($lastPostInfo->hasUserId()) { + $lastPostUserId = $lastPostInfo->getUserId(); + if(!array_key_exists($lastPostUserId, $userInfos)) { + $userInfo = $users->getUser($lastPostInfo->getUserId(), 'id'); + $userInfos[$lastPostUserId] = $userInfo; + $userColours[$lastPostUserId] = $users->getUserColour($userInfo); + } + + $category->lastPost->user = $userInfos[$lastPostUserId]; + $category->lastPost->colour = $userColours[$lastPostUserId]; + } + } + } + } + + $category->perms = $perms; + $category->unread = $unread; } + +Template::render('forum.index', [ + 'forum_categories' => $categories, + 'forum_empty' => empty($categories), + 'forum_show_mark_as_read' => $currentUser !== null, +]); diff --git a/public-legacy/forum/leaderboard.php b/public-legacy/forum/leaderboard.php index 7f2f912..fdd0e04 100644 --- a/public-legacy/forum/leaderboard.php +++ b/public-legacy/forum/leaderboard.php @@ -1,63 +1,116 @@ isLoggedIn() || !perms_check_user(MSZ_PERMS_FORUM, $msz->getActiveUser()->getId(), MSZ_PERM_FORUM_VIEW_LEADERBOARD)) { echo render_error(403); return; } -$leaderboardMode = !empty($_GET['mode']) && is_string($_GET['mode']) && ctype_lower($_GET['mode']) ? $_GET['mode'] : ''; -$leaderboardId = !empty($_GET['id']) && is_string($_GET['id']) - && ctype_digit($_GET['id']) - ? $_GET['id'] - : MSZ_FORUM_LEADERBOARD_CATEGORY_ALL; -$leaderboardIdLength = strlen($leaderboardId); +$forum = $msz->getForum(); +$users = $msz->getUsers(); +$config = $cfg->getValues([ + ['forum_leader.first_year:i', 2018], + ['forum_leader.first_month:i', 12], + 'forum_leader.unranked.forum:a', + 'forum_leader.unranked.topic:a', +]); -$leaderboardYear = $leaderboardIdLength === 4 || $leaderboardIdLength === 6 ? substr($leaderboardId, 0, 4) : null; -$leaderboardMonth = $leaderboardIdLength === 6 ? substr($leaderboardId, 4, 2) : null; +$mode = (string)filter_input(INPUT_GET, 'mode'); +$yearMonth = (string)filter_input(INPUT_GET, 'id'); +$year = $month = 0; -if(empty($_GET['allow_unranked'])) { - [ - 'forum_leader.unranked.forum' => $unrankedForums, - 'forum_leader.unranked.topic' => $unrankedTopics, - ] = $cfg->getValues([ - 'forum_leader.unranked.forum:a', - 'forum_leader.unranked.topic:a', - ]); -} else $unrankedForums = $unrankedTopics = []; +$currentYear = (int)date('Y'); +$currentMonth = (int)date('m'); -$leaderboards = forum_leaderboard_categories(); -$leaderboard = forum_leaderboard_listing($leaderboardYear, $leaderboardMonth, $unrankedForums, $unrankedTopics); +if(!empty($yearMonth)) { + $yearMonthLength = strlen($yearMonth); + if(($yearMonthLength !== 4 && $yearMonthLength !== 6) || !ctype_digit($yearMonth)) { + echo render_error(404); + return; + } -$leaderboardName = 'All Time'; + $year = (int)substr($yearMonth, 0, 4); + if($year < $config['forum_leader.first_year'] || $year > $currentYear) { + echo render_error(404); + return; + } -if($leaderboardYear) { - $leaderboardName = "Leaderboard {$leaderboardYear}"; - - if($leaderboardMonth) - $leaderboardName .= "-{$leaderboardMonth}"; + if($yearMonthLength === 6) { + $month = (int)substr($yearMonth, 4, 2); + if($month < 1 || $month > 12 || ($year === $config['forum_leader.first_year'] && $month < $config['forum_leader.first_month'])) { + echo render_error(404); + return; + } + } } -if($leaderboardMode === 'markdown') { +if(filter_has_var(INPUT_GET, 'allow_unranked')) { + $unrankedForums = $unrankedTopics = []; +} else { + $unrankedForums = $config['forum_leader.unranked.forum']; + $unrankedTopics = $config['forum_leader.unranked.topic']; +} + +$years = $months = []; + +for($i = $currentYear; $i >= $config['forum_leader.first_year']; $i--) + $years[(string)$i] = sprintf('Leaderboard %d', $i); + +for($i = $currentYear, $j = $currentMonth;;) { + $months[sprintf('%d%02d', $i, $j)] = sprintf('Leaderboard %d-%02d', $i, $j); + + if($j <= 1) { + $i--; $j = 12; + } else $j--; + + if($i <= $config['forum_leader.first_year'] && $j < $config['forum_leader.first_month']) + break; +} + +$rankings = $forum->generatePostRankings($year, $month, $unrankedForums, $unrankedTopics); +foreach($rankings as $ranking) { + $ranking->user = $ranking->colour = null; + + if($ranking->userId !== '') + try { + $ranking->user = $users->getUser($ranking->userId); + $ranking->colour = $users->getUserColour($ranking->user); + } catch(RuntimeException $ex) {} +} + +$name = 'All Time'; + +if($year > 0) { + $name = "Leaderboard {$year}"; + + if($month > 0) + $name .= "-{$month}"; +} + +if($mode === 'markdown') { $markdown = << $user['user_id']]), $user['posts']); - } + foreach($rankings as $ranking) + $markdown .= sprintf("| %s | [%s](%s%s) | %s |\r\n", $ranking->position, + $ranking->user?->getName() ?? 'Deleted User', + url_prefix(false), url('user-profile', ['user' => $ranking->userId]), $ranking->postsCount); Template::set('leaderboard_markdown', $markdown); } Template::render('forum.leaderboard', [ - 'leaderboard_id' => $leaderboardId, - 'leaderboard_name' => $leaderboardName, - 'leaderboard_categories' => $leaderboards, - 'leaderboard_data' => $leaderboard, - 'leaderboard_mode' => $leaderboardMode, + 'leaderboard_id' => $yearMonth, + 'leaderboard_name' => $name, + 'leaderboard_years' => $years, + 'leaderboard_months' => $months, + 'leaderboard_data' => $rankings, + 'leaderboard_mode' => $mode, ]); diff --git a/public-legacy/forum/post.php b/public-legacy/forum/post.php index b5cc74b..d22c6e6 100644 --- a/public-legacy/forum/post.php +++ b/public-legacy/forum/post.php @@ -1,6 +1,10 @@ getForum(); + $postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0; $postMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : ''; $submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1'; @@ -20,169 +24,150 @@ if($postMode !== '' && $msz->hasActiveBan()) { return; } -$postInfo = forum_post_get($postId, true); -$perms = empty($postInfo) - ? 0 - : forum_perms_get_user($postInfo['forum_id'], $currentUserId)[MSZ_FORUM_PERMS_GENERAL]; +try { + $postInfo = $forum->getPost(postId: $postId); +} catch(RuntimeException $ex) { + echo render_error(404); + return; +} + +$perms = forum_perms_get_user($postInfo->getCategoryId(), $currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + +if(!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM)) { + echo render_error(403); + return; +} + +$canDeleteAny = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST); switch($postMode) { case 'delete': - $canDelete = forum_post_can_delete($postInfo, $currentUserId); - $canDeleteMsg = ''; - $responseCode = 200; + if($canDeleteAny) { + if($postInfo->isDeleted()) { + echo render_info('This post has already been marked as deleted.', 404); + return; + } + } else { + if($postInfo->isDeleted()) { + echo render_error(404); + return; + } - 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(!perms_check($perms, MSZ_FORUM_PERM_DELETE_POST)) { + echo render_info('You are not allowed to delete posts.', 403); + return; + } + + if($postInfo->getUserId() !== $currentUser->getId()) { + echo render_info('You can only delete your own posts.', 403); + return; + } + + // posts may only be deleted within a week of creation, this should be a config value + $deleteTimeFrame = 60 * 60 * 24 * 7; + if($postInfo->getCreatedTime() < time() - $deleteTimeFrame) { + echo render_info('This post has existed for too long. Ask a moderator to remove if it absolutely necessary.', 403); + return; + } } - if($canDelete !== MSZ_E_FORUM_POST_DELETE_OK) { - echo render_info($canDeleteMsg, $responseCode); - break; + $originalPostInfo = $forum->getPost(topicInfo: $postInfo->getTopicId()); + if($originalPostInfo->getId() === $postInfo->getId()) { + echo render_info('This is the opening post of the topic it belongs to, it may not be deleted without deleting the entire topic as well.', 403); + return; } 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', ], ]); break; } - $deletePost = forum_post_delete($postInfo['post_id']); + $forum->deletePost($postInfo); + $msz->createAuditLog('FORUM_POST_DELETE', [$postInfo->getId()]); - if($deletePost) { - $msz->createAuditLog('FORUM_POST_DELETE', [$postInfo['post_id']]); - } - - if(!$deletePost) { - echo render_error(500); - break; - } - - url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]); + url_redirect('forum-topic', ['topic' => $postInfo->getTopicId()]); break; case 'nuke': - if(!perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST)) { + if(!$canDeleteAny) { echo render_error(403); break; } 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', ], ]); break; } - $nukePost = forum_post_nuke($postInfo['post_id']); + $forum->nukePost($postInfo->getId()); + $msz->createAuditLog('FORUM_POST_NUKE', [$postInfo->getId()]); - if(!$nukePost) { - echo render_error(500); - break; - } - - $msz->createAuditLog('FORUM_POST_NUKE', [$postInfo['post_id']]); - - url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]); + url_redirect('forum-topic', ['topic' => $postInfo->getTopicId()]); break; case 'restore': - if(!perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST)) { + if(!$canDeleteAny) { echo render_error(403); break; } 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', ], ]); break; } - $restorePost = forum_post_restore($postInfo['post_id']); + $forum->restorePost($postInfo->getId()); + $msz->createAuditLog('FORUM_POST_RESTORE', [$postInfo->getId()]); - if(!$restorePost) { - echo render_error(500); - break; - } - - $msz->createAuditLog('FORUM_POST_RESTORE', [$postInfo['post_id']]); - - 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 url_redirect('forum-post', [ - 'post' => $postInfo['post_id'], - 'post_fragment' => 'p' . $postInfo['post_id'], + 'post' => $postInfo->getId(), + 'post_fragment' => 'p' . $postInfo->getId(), ]); break; } diff --git a/public-legacy/forum/posting.php b/public-legacy/forum/posting.php index 1f03d42..ccfc3bc 100644 --- a/public-legacy/forum/posting.php +++ b/public-legacy/forum/posting.php @@ -1,6 +1,10 @@ isLoggedIn()) { @@ -15,6 +19,13 @@ if($msz->hasActiveBan()) { return; } +$forum = $msz->getForum(); +$users = $msz->getUsers(); + +$userInfos = []; +$userColours = []; +$userPostsCounts = []; + $forumPostingModes = [ 'create', 'edit', 'quote', 'preview', ]; @@ -57,42 +68,70 @@ 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)) { + $hasPostInfo = false; +} else { + try { + $postInfo = $forum->getPost(postId: $postId); + } catch(RuntimeException $ex) { + echo render_error(404); + return; } -} -if(!empty($topicId)) { - $topic = forum_topic_get($topicId); - - if(isset($topic['forum_id'])) { - $forumId = (int)$topic['forum_id']; + if($postInfo->isDeleted()) { + echo render_error(404); + return; } + + // should automatic cross-quoting be a thing? if so, check if $topicId is < 1 first <-- what did i mean by this? + $topicId = $postInfo->getTopicId(); + $hasPostInfo = true; } -if(!empty($forumId)) { - $forum = forum_get($forumId); +if(empty($topicId)) { + $hasTopicInfo = false; +} else { + try { + $topicInfo = $forum->getTopic(topicId: $topicId); + } catch(RuntimeException $ex) { + echo render_error(404); + return; + } + + if($topicInfo->isDeleted()) { + echo render_error(404); + return; + } + + $forumId = $topicInfo->getCategoryId(); + $originalPostInfo = $forum->getPost(topicInfo: $topicInfo); + $hasTopicInfo = true; } -if(empty($forum)) { - echo render_error(404); - return; +if(empty($forumId)) { + $hasCategoryInfo = false; +} else { + try { + $categoryInfo = $forum->getCategory(categoryId: $forumId); + } catch(RuntimeException $ex) { + echo render_error(404); + return; + } + + $hasCategoryInfo = true; } -$perms = forum_perms_get_user($forum['forum_id'], $currentUserId)[MSZ_FORUM_PERMS_GENERAL]; +$perms = forum_perms_get_user($categoryInfo->getId(), $currentUserId)[MSZ_FORUM_PERMS_GENERAL]; -if($forum['forum_archived'] - || (!empty($topic['topic_locked']) && !perms_check($perms, MSZ_FORUM_PERM_LOCK_TOPIC)) +if($categoryInfo->isArchived() + || (isset($topicInfo) && $topicInfo->isLocked() && !perms_check($perms, MSZ_FORUM_PERM_LOCK_TOPIC)) || !perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM | MSZ_FORUM_PERM_CREATE_POST) - || (empty($topic) && !perms_check($perms, MSZ_FORUM_PERM_CREATE_TOPIC))) { + || (!isset($topicInfo) && !perms_check($perms, MSZ_FORUM_PERM_CREATE_TOPIC))) { echo render_error(403); return; } -if(!forum_may_have_topics($forum['forum_type'])) { +if(!$categoryInfo->mayHaveTopics()) { echo render_error(400); return; } @@ -100,30 +139,20 @@ if(!forum_may_have_topics($forum['forum_type'])) { $topicTypes = []; if($mode === 'create' || $mode === 'edit') { - $topicTypes[MSZ_TOPIC_TYPE_DISCUSSION] = 'Normal discussion'; + $topicTypes['discussion'] = 'Normal discussion'; - if(perms_check($perms, MSZ_FORUM_PERM_STICKY_TOPIC)) { - $topicTypes[MSZ_TOPIC_TYPE_STICKY] = 'Sticky topic'; - } - if(perms_check($perms, MSZ_FORUM_PERM_ANNOUNCE_TOPIC)) { - $topicTypes[MSZ_TOPIC_TYPE_ANNOUNCEMENT] = 'Announcement'; - } - if(perms_check($perms, MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC)) { - $topicTypes[MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT] = 'Global Announcement'; - } + if(perms_check($perms, MSZ_FORUM_PERM_STICKY_TOPIC)) + $topicTypes['sticky'] = 'Sticky topic'; + if(perms_check($perms, MSZ_FORUM_PERM_ANNOUNCE_TOPIC)) + $topicTypes['announce'] = 'Announcement'; + if(perms_check($perms, MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC)) + $topicTypes['global'] = 'Global Announcement'; } // edit mode stuff -if($mode === 'edit') { - if(empty($post)) { - echo render_error(404); - return; - } - - if(!perms_check($perms, (string)$post['poster_id'] === $currentUserId ? MSZ_FORUM_PERM_EDIT_POST : MSZ_FORUM_PERM_EDIT_ANY_POST)) { - echo render_error(403); - return; - } +if($mode === 'edit' && !perms_check($perms, $postInfo->getUserId() === $currentUserId ? MSZ_FORUM_PERM_EDIT_POST : MSZ_FORUM_PERM_EDIT_ANY_POST)) { + echo render_error(403); + return; } $notices = []; @@ -132,38 +161,50 @@ 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']) ? $_POST['post']['type'] : null; $postSignature = isset($_POST['post']['signature']); if(!CSRF::validateRequest()) { $notices[] = 'Could not verify request.'; } else { - $isEditingTopic = empty($topic) || ($mode === 'edit' && $post['is_opening_post']); + $isEditingTopic = empty($topicInfo) || ($mode === 'edit' && $originalPostInfo->getId() == $postInfo->getId()); + + if(is_string($topicType)) + $topicType = ForumTopicInfo::TYPE_ALIASES[$topicType] ?? ForumTopicInfo::TYPE_DISCUSSION; + else + $topicType = (int)$topicType; if($mode === 'create') { - $timeoutCheck = max(1, forum_timeout($forumId, $currentUserId)); + $postTimeout = $cfg->getInteger('forum.posting.timeout', 5); + if($postTimeout > 0) { + $postTimeoutThreshold = DateTime::now()->modify(sprintf('-%d seconds', $postTimeout)); + $lastPostCreatedAt = $forum->getUserLastPostCreatedAt($currentUser); - if($timeoutCheck < 5) { - $notices[] = sprintf("You're posting too quickly! Please wait %s seconds before posting again.", number_format($timeoutCheck)); - $notices[] = "It's possible that your post went through successfully and you pressed the submit button twice by accident."; + if($lastPostCreatedAt->isMoreThan($postTimeoutThreshold)) { + $waitSeconds = $postTimeout + ($lastPostCreatedAt->getUnixTimeSeconds() - time()); + + $notices[] = sprintf("You're posting too quickly! Please wait %s seconds before posting again.", number_format($waitSeconds)); + $notices[] = "It's possible that your post went through successfully and you pressed the submit button twice by accident."; + } } } if($isEditingTopic) { - $originalTopicTitle = $topic['topic_title'] ?? null; + $originalTopicTitle = $topicInfo?->getTitle() ?? null; $topicTitleChanged = $topicTitle !== $originalTopicTitle; - $originalTopicType = (int)($topic['topic_type'] ?? MSZ_TOPIC_TYPE_DISCUSSION); + $originalTopicType = (int)($topicInfo?->getType() ?? 0); $topicTypeChanged = $topicType !== null && $topicType !== $originalTopicType; - switch(forum_validate_title($topicTitle)) { - case 'too-short': - $notices[] = 'Topic title was too short.'; - break; + $topicTitleLengths = $cfg->getValues([ + ['forum.topic.minLength:i', 3], + ['forum.topic.maxLength:i', 100], + ]); - case 'too-long': - $notices[] = 'Topic title was too long.'; - break; - } + $topicTitleLength = mb_strlen(trim($topicTitle)); + if($topicTitleLength < $topicTitleLengths['forum.topic.minLength']) + $notices[] = 'Topic title was too short.'; + elseif($topicTitleLength > $topicTitleLengths['forum.topic.maxLength']) + $notices[] = 'Topic title was too long.'; if($mode === 'create' && $topicType === null) { $topicType = array_key_first($topicTypes); @@ -172,66 +213,76 @@ if(!empty($_POST)) { } } - if(!Parser::isValid($postParser)) { + if(!Parser::isValid($postParser)) $notices[] = 'Invalid parser selected.'; - } - switch(forum_validate_post($postText)) { - case 'too-short': - $notices[] = 'Post content was too short.'; - break; + $postTextLengths = $cfg->getValues([ + ['forum.post.minLength:i', 1], + ['forum.post.maxLength:i', 60000], + ]); - case 'too-long': - $notices[] = 'Post content was too long.'; - break; - } + $postTextLength = mb_strlen(trim($postText)); + if($postTextLength < $postTextLengths['forum.post.minLength']) + $notices[] = 'Post content was too short.'; + elseif($postTextLength > $postTextLengths['forum.post.maxLength']) + $notices[] = 'Post content was too long.'; if(empty($notices)) { switch($mode) { case 'create': - if(!empty($topic)) { - forum_topic_bump($topic['topic_id']); - } else { - $topicId = forum_topic_create( - $forum['forum_id'], - $currentUserId, + if(empty($topicInfo)) { + $topicInfo = $forum->createTopic( + $categoryInfo, + $currentUser, $topicTitle, $topicType ); - } - $postId = forum_post_create( + $topicId = $topicInfo->getId(); + $forum->incrementCategoryTopics($categoryInfo); + } else + $forum->bumpTopic($topicInfo); + + $postInfo = $forum->createPost( $topicId, - $forum['forum_id'], - $currentUserId, + $currentUser, $_SERVER['REMOTE_ADDR'], $postText, $postParser, - $postSignature + $postSignature, + $categoryInfo ); - forum_topic_mark_read($currentUserId, $topicId, $forum['forum_id']); - forum_count_increase($forum['forum_id'], empty($topic)); + + $postId = $postInfo->getId(); + $forum->incrementCategoryPosts($categoryInfo); break; case 'edit': - $markUpdated = $post['poster_id'] === $currentUserId - && $post['post_created_unix'] < strtotime('-1 minutes') - && $postText !== $post['post_text']; + $markUpdated = $postInfo->getUserId() === $currentUserId + && $postInfo->shouldMarkAsEdited() + && $postText !== $postInfo->getBody(); - if(!forum_post_update($postId, $_SERVER['REMOTE_ADDR'], $postText, $postParser, $postSignature, $markUpdated)) { - $notices[] = 'Post edit failed.'; - } + $forum->updatePost( + $postId, + remoteAddr: $_SERVER['REMOTE_ADDR'], + body: $postText, + bodyParser: $postParser, + displaySignature: $postSignature, + bumpEdited: $markUpdated + ); - if($isEditingTopic && ($topicTitleChanged || $topicTypeChanged)) { - if(!forum_topic_update($topicId, $topicTitle, $topicType)) { - $notices[] = 'Topic update failed.'; - } - } + if($isEditingTopic && ($topicTitleChanged || $topicTypeChanged)) + $forum->updateTopic( + $topicId, + title: $topicTitle, + type: $topicType + ); break; } if(empty($notices)) { - $redirect = url(empty($topic) ? 'forum-topic' : 'forum-post', [ + // does this ternary ever return forum-topic? + $redirect = url(empty($topicInfo) ? 'forum-topic' : 'forum-post', [ 'topic' => $topicId ?? 0, 'post' => $postId ?? 0, 'post_fragment' => 'p' . ($postId ?? 0), @@ -243,24 +294,61 @@ if(!empty($_POST)) { } } -if(!empty($topic)) { - Template::set('posting_topic', $topic); -} +if(!empty($topicInfo)) + Template::set('posting_topic', $topicInfo); if($mode === 'edit') { // $post is pretty much sure to be populated at this point + $post = new stdClass; + $post->info = $postInfo; + + if($postInfo->hasUserId()) { + $postUserId = $postInfo->getUserId(); + if(!array_key_exists($postUserId, $userInfos)) { + $userInfo = $users->getUser($postUserId, 'id'); + $userInfos[$postUserId] = $userInfo; + $userColours[$postUserId] = $users->getUserColour($userInfo); + $userPostsCounts[$postUserId] = $forum->countPosts(userInfo: $userInfo, deleted: false); + } + + $post->user = $userInfos[$postUserId]; + $post->colour = $userColours[$postUserId]; + $post->postsCount = $userPostsCounts[$postUserId]; + } + + $post->isOriginalPost = $originalPostInfo->getId() == $postInfo->getId(); + $post->isOriginalPoster = $originalPostInfo->hasUserId() && $postInfo->hasUserId() + && $originalPostInfo->getUserId() === $postInfo->getUserId(); + Template::set('posting_post', $post); } -$displayInfo = forum_posting_info($currentUserId); +try { + $lastPostInfo = $forum->getPost(userInfo: $currentUser, getLast: true, deleted: false); + $selectedParser = $lastPostInfo->getParser(); +} catch(RuntimeException $ex) { + $selectedParser = Parser::BBCODE; +} + +// this sucks, fix it! +$topicTypeName = match($topicType ?? $topicInfo?->getType() ?? null) { + default => 'discussion', + ForumTopicInfo::TYPE_STICKY => 'sticky', + ForumTopicInfo::TYPE_ANNOUNCE => 'announce', + ForumTopicInfo::TYPE_GLOBAL => 'global', +}; Template::render('forum.posting', [ - 'posting_breadcrumbs' => forum_get_breadcrumbs($forumId), - 'global_accent_colour' => forum_get_colour($forumId), - 'posting_forum' => $forum, - 'posting_info' => $displayInfo, + 'posting_breadcrumbs' => $forum->getCategoryAncestry($categoryInfo), + 'global_accent_colour' => $forum->getCategoryColour($categoryInfo), + 'posting_user' => $currentUser, + 'posting_user_colour' => $userColours[$currentUser->getId()] ?? $users->getUserColour($currentUser), + 'posting_user_posts_count' => $userPostsCounts[$currentUser->getId()] ?? $forum->countPosts(userInfo: $currentUser, deleted: false), + 'posting_user_preferred_parser' => $selectedParser, + 'posting_forum' => $categoryInfo, 'posting_notices' => $notices, 'posting_mode' => $mode, 'posting_types' => $topicTypes, + 'posting_type_selected' => $topicTypeName, 'posting_defaults' => [ 'title' => $topicTitle ?? null, 'type' => $topicType ?? null, diff --git a/public-legacy/forum/topic.php b/public-legacy/forum/topic.php index 90cedfd..7a0a36f 100644 --- a/public-legacy/forum/topic.php +++ b/public-legacy/forum/topic.php @@ -1,43 +1,76 @@ getForum(); +$users = $msz->getUsers(); + $postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0; $topicId = !empty($_GET['t']) && is_string($_GET['t']) ? (int)$_GET['t'] : 0; +$categoryId = null; $moderationMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : ''; $submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1'; -$topicUser = $msz->getActiveUser(); -$topicUserId = $topicUser === null ? '0' : $topicUser->getId(); +$currentUser = $msz->getActiveUser(); +$currentUserId = $currentUser === null ? '0' : $currentUser->getId(); if($topicId < 1 && $postId > 0) { - $postInfo = forum_post_find($postId, $topicUserId); - - if(!empty($postInfo['topic_id'])) { - $topicId = (int)$postInfo['topic_id']; + try { + $postInfo = $forum->getPost(postId: $postId); + } catch(RuntimeException $ex) { + echo render_error(404); + return; } + + $categoryId = $postInfo->getCategoryId(); + $perms = forum_perms_get_user($categoryId, $currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + $canDeleteAny = !perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST); + + if($postInfo->isDeleted() && !$canDeleteAny) { + echo render_error(404); + return; + } + + $topicId = $postInfo->getTopicId(); + $preceedingPostCount = $forum->countPosts( + topicInfo: $topicId, + upToPostInfo: $postInfo, + deleted: $canDeleteAny ? null : false + ); } -$topic = forum_topic_get($topicId, true); -$perms = $topic - ? forum_perms_get_user($topic['forum_id'], $topicUserId)[MSZ_FORUM_PERMS_GENERAL] - : 0; +try { + $topicIsNuked = $topicIsDeleted = $canDeleteAny = false; + $topicInfo = $forum->getTopic(topicId: $topicId); +} catch(RuntimeException $ex) { + $topicIsNuked = true; +} -if(isset($topicUser) && $msz->hasActiveBan($topicUser)) - $perms &= ~MSZ_FORUM_PERM_SET_WRITE; +if(!$topicIsNuked) { + $topicIsDeleted = $topicInfo->isDeleted(); -$topicIsNuked = empty($topic['topic_id']); -$topicIsDeleted = !empty($topic['topic_deleted']); -$canDeleteAny = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST); + if($categoryId !== (int)$topicInfo->getCategoryId()) { + $categoryId = (int)$topicInfo->getCategoryId(); + $perms = forum_perms_get_user($categoryId, $currentUserId)[MSZ_FORUM_PERMS_GENERAL]; + } -if($topicIsNuked || $topicIsDeleted) { - $topicRedirectInfo = forum_topic_redir_info($topicId); + if(isset($currentUser) && $msz->hasActiveBan($currentUser)) + $perms &= MSZ_FORUM_PERM_LIST_FORUM | MSZ_FORUM_PERM_VIEW_FORUM; + + $canDeleteAny = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST); +} + +if(($topicIsNuked || $topicIsDeleted) && $forum->hasTopicRedirect($topicId)) { + $topicRedirectInfo = $forum->getTopicRedirect($topicId); Template::set('topic_redir_info', $topicRedirectInfo); if($topicIsNuked || !$canDeleteAny) { if(empty($topicRedirectInfo)) echo render_error(404); else - header('Location: ' . $topicRedirectInfo->topic_redir_url); + header('Location: ' . $topicRedirectInfo->getLinkTarget()); return; } } @@ -47,9 +80,14 @@ if(!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM)) { return; } -$topicIsLocked = !empty($topic['topic_locked']); -$topicIsArchived = !empty($topic['topic_archived']); -$topicPostsTotal = (int)($topic['topic_count_posts'] + $topic['topic_count_posts_deleted']); +// Maximum amount of posts a topic may contain to still be deletable by the author +// this should be in the config +$deletePostThreshold = 1; + +$categoryInfo = $forum->getCategory(topicInfo: $topicInfo); +$topicIsLocked = $topicInfo->isLocked(); +$topicIsArchived = $categoryInfo->isArchived(); +$topicPostsTotal = $topicInfo->getTotalPostsCount(); $topicIsFrozen = $topicIsArchived || $topicIsDeleted; $canDeleteOwn = !$topicIsFrozen && !$topicIsLocked && perms_check($perms, MSZ_FORUM_PERM_DELETE_POST); $canBumpTopic = !$topicIsFrozen && perms_check($perms, MSZ_FORUM_PERM_BUMP_TOPIC); @@ -58,9 +96,9 @@ $canNukeOrRestore = $canDeleteAny && $topicIsDeleted; $canDelete = !$topicIsDeleted && ( $canDeleteAny || ( $topicPostsTotal > 0 - && $topicPostsTotal <= MSZ_FORUM_TOPIC_DELETE_POST_LIMIT + && $topicPostsTotal <= $deletePostThreshold && $canDeleteOwn - && $topic['author_user_id'] === $topicUserId + && $topicInfo->getUserId() === (string)$currentUserId ) ); @@ -87,58 +125,49 @@ if(in_array($moderationMode, $validModerationModes, true)) { switch($moderationMode) { case 'delete': - $canDeleteCode = forum_topic_can_delete($topic, $topicUserId); - $canDeleteMsg = ''; - $responseCode = 200; + if($canDeleteAny) { + if($topicInfo->isDeleted()) { + echo render_info('This topic has already been marked as deleted.', 404); + return; + } + } else { + if($topicInfo->isDeleted()) { + echo render_error(404); + return; + } - switch($canDeleteCode) { - case MSZ_E_FORUM_TOPIC_DELETE_USER: - $responseCode = 401; - $canDeleteMsg = 'You must be logged in to delete topics.'; - break; - case MSZ_E_FORUM_TOPIC_DELETE_TOPIC: - $responseCode = 404; - $canDeleteMsg = "This topic doesn't exist."; - break; - case MSZ_E_FORUM_TOPIC_DELETE_DELETED: - $responseCode = 404; - $canDeleteMsg = 'This topic has already been marked as deleted.'; - break; - case MSZ_E_FORUM_TOPIC_DELETE_OWNER: - $responseCode = 403; - $canDeleteMsg = 'You can only delete your own topics.'; - break; - case MSZ_E_FORUM_TOPIC_DELETE_OLD: - $responseCode = 401; - $canDeleteMsg = 'This topic has existed for too long. Ask a moderator to remove if it absolutely necessary.'; - break; - case MSZ_E_FORUM_TOPIC_DELETE_PERM: - $responseCode = 401; - $canDeleteMsg = 'You are not allowed to delete topics.'; - break; - case MSZ_E_FORUM_TOPIC_DELETE_POSTS: - $responseCode = 403; - $canDeleteMsg = 'This topic already has replies, you may no longer delete it. Ask a moderator to remove if it absolutely necessary.'; - break; - case MSZ_E_FORUM_TOPIC_DELETE_OK: - break; - default: - $responseCode = 500; - $canDeleteMsg = sprintf('Unknown error \'%d\'', $canDelete); - } + if(!$canDeleteOwn) { + echo render_info("You aren't allowed to delete topics.", 403); + return; + } - if($canDeleteCode !== MSZ_E_FORUM_TOPIC_DELETE_OK) { - echo render_info($canDeleteMsg, $responseCode); - break; + if($topicInfo->getUserId() !== $currentUser->getId()) { + echo render_info('You can only delete your own topics.', 403); + return; + } + + // topics may only be deleted within a day of creation, this should be a config value + $deleteTimeFrame = 60 * 60 * 24; + if($topicInfo->getCreatedTime() < time() - $deleteTimeFrame) { + echo render_info('This topic has existed for too long. Ask a moderator to remove if it absolutely necessary.', 403); + return; + } + + // deleted posts are intentionally included + $topicPostCount = $forum->countPosts(topicInfo: $topicInfo); + if($topicPostCount > $deletePostThreshold) { + echo render_info('This topic already has replies, you may no longer delete it. Ask a moderator to remove if it absolutely necessary.', 403); + return; + } } if(!isset($_GET['confirm'])) { Template::render('forum.confirm', [ 'title' => 'Confirm topic deletion', 'class' => 'far fa-trash-alt', - 'message' => sprintf('You are about to delete topic #%d. Are you sure about that?', $topic['topic_id']), + 'message' => sprintf('You are about to delete topic #%d. Are you sure about that?', $topicInfo->getId()), 'params' => [ - 't' => $topic['topic_id'], + 't' => $topicInfo->getId(), 'm' => 'delete', ], ]); @@ -146,23 +175,16 @@ if(in_array($moderationMode, $validModerationModes, true)) { } elseif(!$submissionConfirmed) { url_redirect( 'forum-topic', - ['topic' => $topic['topic_id']] + ['topic' => $topicInfo->getId()] ); break; } - $deleteTopic = forum_topic_delete($topic['topic_id']); - - if($deleteTopic) - $msz->createAuditLog('FORUM_TOPIC_DELETE', [$topic['topic_id']]); - - if(!$deleteTopic) { - echo render_error(500); - break; - } + $forum->deleteTopic($topicInfo->getId()); + $msz->createAuditLog('FORUM_TOPIC_DELETE', [$topicInfo->getId()]); url_redirect('forum-category', [ - 'forum' => $topic['forum_id'], + 'forum' => $categoryInfo->getId(), ]); break; @@ -176,31 +198,25 @@ if(in_array($moderationMode, $validModerationModes, true)) { Template::render('forum.confirm', [ 'title' => 'Confirm topic restore', 'class' => 'fas fa-magic', - 'message' => sprintf('You are about to restore topic #%d. Are you sure about that?', $topic['topic_id']), + 'message' => sprintf('You are about to restore topic #%d. Are you sure about that?', $topicInfo->getId()), 'params' => [ - 't' => $topic['topic_id'], + 't' => $topicInfo->getId(), 'm' => 'restore', ], ]); break; } elseif(!$submissionConfirmed) { url_redirect('forum-topic', [ - 'topic' => $topic['topic_id'], + 'topic' => $topicInfo->getId(), ]); break; } - $restoreTopic = forum_topic_restore($topic['topic_id']); - - if(!$restoreTopic) { - echo render_error(500); - break; - } - - $msz->createAuditLog('FORUM_TOPIC_RESTORE', [$topic['topic_id']]); + $forum->restoreTopic($topicInfo->getId()); + $msz->createAuditLog('FORUM_TOPIC_RESTORE', [$topicInfo->getId()]); url_redirect('forum-category', [ - 'forum' => $topic['forum_id'], + 'forum' => $categoryInfo->getId(), ]); break; @@ -214,112 +230,139 @@ if(in_array($moderationMode, $validModerationModes, true)) { Template::render('forum.confirm', [ 'title' => 'Confirm topic nuke', 'class' => 'fas fa-radiation', - 'message' => sprintf('You are about to PERMANENTLY DELETE topic #%d. Are you sure about that?', $topic['topic_id']), + 'message' => sprintf('You are about to PERMANENTLY DELETE topic #%d. Are you sure about that?', $topicInfo->getId()), 'params' => [ - 't' => $topic['topic_id'], + 't' => $topicInfo->getId(), 'm' => 'nuke', ], ]); break; } elseif(!$submissionConfirmed) { url_redirect('forum-topic', [ - 'topic' => $topic['topic_id'], + 'topic' => $topicInfo->getId(), ]); break; } - $nukeTopic = forum_topic_nuke($topic['topic_id']); - - if(!$nukeTopic) { - echo render_error(500); - break; - } - - $msz->createAuditLog('FORUM_TOPIC_NUKE', [$topic['topic_id']]); + $forum->nukeTopic($topicInfo->getId()); + $msz->createAuditLog('FORUM_TOPIC_NUKE', [$topicInfo->getId()]); url_redirect('forum-category', [ - 'forum' => $topic['forum_id'], + 'forum' => $categoryInfo->getId(), ]); break; case 'bump': - if($canBumpTopic && forum_topic_bump($topic['topic_id'])) { - $msz->createAuditLog('FORUM_TOPIC_BUMP', [$topic['topic_id']]); + if($canBumpTopic) { + $forum->bumpTopic($topicInfo->getId()); + $msz->createAuditLog('FORUM_TOPIC_BUMP', [$topicInfo->getId()]); } url_redirect('forum-topic', [ - 'topic' => $topic['topic_id'], + 'topic' => $topicInfo->getId(), ]); break; case 'lock': - if($canLockTopic && !$topicIsLocked && forum_topic_lock($topic['topic_id'])) { - $msz->createAuditLog('FORUM_TOPIC_LOCK', [$topic['topic_id']]); + if($canLockTopic && !$topicIsLocked) { + $forum->lockTopic($topicInfo->getId()); + $msz->createAuditLog('FORUM_TOPIC_LOCK', [$topicInfo->getId()]); } url_redirect('forum-topic', [ - 'topic' => $topic['topic_id'], + 'topic' => $topicInfo->getId(), ]); break; case 'unlock': - if($canLockTopic && $topicIsLocked && forum_topic_unlock($topic['topic_id'])) { - $msz->createAuditLog('FORUM_TOPIC_UNLOCK', [$topic['topic_id']]); + if($canLockTopic && $topicIsLocked) { + $forum->unlockTopic($topicInfo->getId()); + $msz->createAuditLog('FORUM_TOPIC_UNLOCK', [$topicInfo->getId()]); } url_redirect('forum-topic', [ - 'topic' => $topic['topic_id'], + 'topic' => $topicInfo->getId(), ]); break; } return; } -$topicPosts = $topic['topic_count_posts']; +$topicPosts = $topicInfo->getPostsCount(); +if($canDeleteAny) + $topicPosts += $topicInfo->getDeletedPostsCount(); -if($canDeleteAny) { - $topicPosts += $topic['topic_count_posts_deleted']; -} +$topicPagination = new Pagination($topicPosts, 10, 'page'); -$topicPagination = new Pagination($topicPosts, MSZ_FORUM_POSTS_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($preceedingPostCount)) + $topicPagination->setPage(floor($preceedingPostCount / $topicPagination->getRange()), true); if(!$topicPagination->hasValidOffset()) { echo render_error(404); return; } -Template::set('topic_perms', $perms); - -$posts = forum_post_listing( - $topic['topic_id'], - $topicPagination->getOffset(), - $topicPagination->getRange(), - perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST) +$postInfos = $forum->getPosts( + topicInfo: $topicInfo, + deleted: perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST) ? null : false, + pagination: $topicPagination, ); -if(!$posts) { +if(empty($postInfos)) { echo render_error(404); return; } +$originalPostInfo = $forum->getPost(topicInfo: $topicInfo); + +$userInfos = []; +$userColours = []; +$userPostsCounts = []; +$posts = []; + +foreach($postInfos as $postInfo) { + $posts[] = $post = new stdClass; + $post->info = $postInfo; + + if($postInfo->hasUserId()) { + $postUserId = $postInfo->getUserId(); + if(!array_key_exists($postUserId, $userInfos)) { + $userInfo = $users->getUser($postUserId, 'id'); + $userInfos[$postUserId] = $userInfo; + $userColours[$postUserId] = $users->getUserColour($userInfo); + $userPostsCounts[$postUserId] = $forum->countPosts(userInfo: $userInfo, deleted: false); + } + + $post->user = $userInfos[$postUserId]; + $post->colour = $userColours[$postUserId]; + $post->postsCount = $userPostsCounts[$postUserId]; + } + + $post->isOriginalPost = $originalPostInfo->getId() == $postInfo->getId(); + $post->isOriginalPoster = $originalPostInfo->hasUserId() && $postInfo->hasUserId() + && $originalPostInfo->getUserId() === $postInfo->getUserId(); +} + $canReply = !$topicIsArchived && !$topicIsLocked && !$topicIsDeleted && perms_check($perms, MSZ_FORUM_PERM_CREATE_POST); -forum_topic_mark_read($topicUserId, $topic['topic_id'], $topic['forum_id']); +if(!$forum->checkUserHasReadTopic($userInfo, $topicInfo)) + $forum->incrementTopicView($topicInfo); + +$forum->updateUserReadTopic($currentUser, $topicInfo); + +$perms = perms_check_bulk($perms, [ + 'can_create_post' => MSZ_FORUM_PERM_CREATE_POST, + 'can_edit_post' => MSZ_FORUM_PERM_EDIT_POST, + 'can_edit_any_post' => MSZ_FORUM_PERM_EDIT_ANY_POST, + 'can_delete_post' => MSZ_FORUM_PERM_DELETE_POST, + 'can_delete_any_post' => MSZ_FORUM_PERM_DELETE_ANY_POST, +]); Template::render('forum.topic', [ - 'topic_breadcrumbs' => forum_get_breadcrumbs($topic['forum_id']), - 'global_accent_colour' => forum_get_colour($topic['forum_id']), - 'topic_info' => $topic, + 'topic_breadcrumbs' => $forum->getCategoryAncestry($topicInfo), + 'global_accent_colour' => $forum->getCategoryColour($topicInfo), + 'topic_info' => $topicInfo, + 'category_info' => $categoryInfo, 'topic_posts' => $posts, 'can_reply' => $canReply, 'topic_pagination' => $topicPagination, @@ -327,5 +370,6 @@ Template::render('forum.topic', [ 'topic_can_nuke_or_restore' => $canNukeOrRestore, 'topic_can_bump' => $canBumpTopic, 'topic_can_lock' => $canLockTopic, - 'topic_user_id' => $topicUserId, + 'topic_user_id' => $currentUserId, + 'topic_perms' => $perms, ]); diff --git a/public-legacy/manage/forum/category.php b/public-legacy/manage/forum/category.php deleted file mode 100644 index 16f62c9..0000000 --- a/public-legacy/manage/forum/category.php +++ /dev/null @@ -1,22 +0,0 @@ -isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_FORUM_MANAGE_FORUMS)) { - echo render_error(403); - return; -} - -$getForum = DB::prepare(' - SELECT * - FROM `msz_forum_categories` - WHERE `forum_id` = :forum_id -'); -$getForum->bind('forum_id', (int)($_GET['f'] ?? 0)); -$forum = $getForum->fetch(); - -if(!$forum) { - echo render_error(404); - return; -} - -Template::render('manage.forum.forum', compact('forum')); diff --git a/public-legacy/manage/forum/index.php b/public-legacy/manage/forum/index.php index 2032c11..9c71392 100644 --- a/public-legacy/manage/forum/index.php +++ b/public-legacy/manage/forum/index.php @@ -6,7 +6,6 @@ if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUs return; } -$forums = DB::query('SELECT * FROM `msz_forum_categories`')->fetchAll(); $rawPerms = perms_create(MSZ_FORUM_PERM_MODES); $perms = manage_forum_perms_list($rawPerms); @@ -16,4 +15,4 @@ if(!empty($_POST['perms']) && is_array($_POST['perms'])) { Template::set('calculated_perms', $finalPerms); } -Template::render('manage.forum.listing', compact('forums', 'perms')); +Template::render('manage.forum.listing', compact('perms')); diff --git a/public-legacy/manage/forum/redirs.php b/public-legacy/manage/forum/redirs.php index 4454ec2..a892a0f 100644 --- a/public-legacy/manage/forum/redirs.php +++ b/public-legacy/manage/forum/redirs.php @@ -6,18 +6,17 @@ if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUs return; } +$forum = $msz->getForum(); + if($_SERVER['REQUEST_METHOD'] === 'POST') { if(!CSRF::validateRequest()) throw new \Exception("Request verification failed."); - $rTopicId = (int)filter_input(INPUT_POST, 'topic_redir_id'); + $rTopicId = (string)filter_input(INPUT_POST, 'topic_redir_id'); $rTopicURL = trim((string)filter_input(INPUT_POST, 'topic_redir_url')); - if($rTopicId < 1) - throw new \Exception("Invalid topic id."); - $msz->createAuditLog('FORUM_TOPIC_REDIR_CREATE', [$rTopicId]); - forum_topic_redir_create($rTopicId, $msz->getActiveUser()->getId(), $rTopicURL); + $forum->createTopicRedirect($rTopicId, $msz->getActiveUser(), $rTopicURL); url_redirect('manage-forum-topic-redirs'); return; } @@ -26,20 +25,20 @@ if(filter_input(INPUT_GET, 'm') === 'explode') { if(!CSRF::validateRequest()) throw new \Exception("Request verification failed."); - $rTopicId = (int)filter_input(INPUT_GET, 't'); + $rTopicId = (string)filter_input(INPUT_GET, 't'); $msz->createAuditLog('FORUM_TOPIC_REDIR_REMOVE', [$rTopicId]); - forum_topic_redir_remove($rTopicId); + $forum->deleteTopicRedirect($rTopicId); url_redirect('manage-forum-topic-redirs'); return; } -$pagination = new Pagination(forum_topic_redir_count(), 20); +$pagination = new Pagination($forum->countTopicRedirects(), 20); if(!$pagination->hasValidOffset()) { echo render_error(404); return; } -$redirs = forum_topic_redir_all($pagination->getOffset(), $pagination->getRange()); +$redirs = $forum->getTopicRedirects(pagination: $pagination); Template::render('manage.forum.redirs', [ 'manage_redirs' => $redirs, diff --git a/public-legacy/members.php b/public-legacy/members.php index 37e97c0..293f025 100644 --- a/public-legacy/members.php +++ b/public-legacy/members.php @@ -12,6 +12,7 @@ if(!$msz->isLoggedIn()) { $users = $msz->getUsers(); $roles = $msz->getRoles(); +$forum = $msz->getForum(); $roleId = filter_has_var(INPUT_GET, 'r') ? (string)filter_input(INPUT_GET, 'r') : null; $orderBy = strtolower((string)filter_input(INPUT_GET, 'ss')); @@ -97,8 +98,8 @@ foreach($userInfos as $userInfo) $userList[] = [ 'info' => $userInfo, 'colour' => $users->getUserColour($userInfo), - 'ftopics' => forum_get_user_topic_count($userInfo), - 'fposts' => forum_get_user_post_count($userInfo), + 'ftopics' => $forum->countTopics(userInfo: $userInfo, deleted: false), + 'fposts' => $forum->countPosts(userInfo: $userInfo, deleted: false), ]; if(empty($userList)) diff --git a/public-legacy/profile.php b/public-legacy/profile.php index 14369d8..49a807b 100644 --- a/public-legacy/profile.php +++ b/public-legacy/profile.php @@ -15,6 +15,7 @@ $profileMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] $isEditing = !empty($_GET['edit']) && is_string($_GET['edit']) ? (bool)$_GET['edit'] : !empty($_POST) && is_array($_POST); $users = $msz->getUsers(); +$forum = $msz->getForum(); $viewerInfo = $msz->getActiveUser(); $viewingAsGuest = $viewerInfo === null; @@ -42,6 +43,23 @@ if($userInfo->isDeleted()) { return; } +switch($profileMode) { + default: + echo render_error(404); + return; + + case 'forum-topics': + url_redirect('search-query', ['query' => sprintf('type:forum:topic author:%s', $userInfo->getName()), 'section' => 'topics']); + return; + + case 'forum-posts': + url_redirect('search-query', ['query' => sprintf('type:forum:post author:%s', $userInfo->getName()), 'section' => 'posts']); + return; + + case '': + break; +} + $notices = []; $userRank = $users->getUserRank($userInfo); @@ -324,137 +342,91 @@ $profileStats = DB::prepare(' WHERE `user_id` = :user_id ')->bind('user_id', $userInfo->getId())->fetch(); -switch($profileMode) { - default: - echo render_error(404); - return; +if(!$viewingAsGuest) { + Template::set('profile_warnings', $msz->getWarnings()->getWarningsWithDefaultBacklog($userInfo)); - case 'forum-topics': - $template = 'profile.topics'; - $topicsCount = forum_topic_count_user($userInfo->getId(), $viewerId); - $topicsPagination = new Pagination($topicsCount, 20); - - if(!$topicsPagination->hasValidOffset()) { - echo render_error(404); - return; - } - - $topics = forum_topic_listing_user( - $userInfo->getId(), $viewerId, - $topicsPagination->getOffset(), $topicsPagination->getRange() - ); - - Template::set([ - 'title' => $userInfo->getName() . ' / topics', - 'canonical_url' => url('user-profile-forum-topics', ['user' => $userInfo->getId(), 'page' => Pagination::param()]), - 'profile_topics' => $topics, - 'profile_topics_pagination' => $topicsPagination, + if((!$isBanned || $canEdit)) { + $unranked = $cfg->getValues([ + 'forum_leader.unranked.forum:a', + 'forum_leader.unranked.topic:a', ]); - break; - case 'forum-posts': - $template = 'profile.posts'; - $postsCount = forum_post_count_user($userInfo->getId()); - $postsPagination = new Pagination($postsCount, 20); - - if(!$postsPagination->hasValidOffset()) { - echo render_error(404); - return; - } - - $posts = forum_post_listing( - $userInfo->getId(), - $postsPagination->getOffset(), - $postsPagination->getRange(), - false, - true + $activeCategoryStats = $forum->getMostActiveCategoryInfo( + $userInfo, + $unranked['forum_leader.unranked.forum'], + $unranked['forum_leader.unranked.topic'], + deleted: false ); + $activeCategoryInfo = $activeCategoryStats->success ? $forum->getCategory(categoryId: $activeCategoryStats->categoryId) : null; - Template::set([ - 'title' => $userInfo->getName() . ' / posts', - 'canonical_url' => url('user-profile-forum-posts', ['user' => $userInfo->getId(), 'page' => Pagination::param()]), - 'profile_posts' => $posts, - 'profile_posts_pagination' => $postsPagination, - ]); - break; + $activeTopicStats = $forum->getMostActiveTopicInfo( + $userInfo, + $unranked['forum_leader.unranked.forum'], + $unranked['forum_leader.unranked.topic'], + deleted: false + ); + $activeTopicInfo = $activeTopicStats->success ? $forum->getTopic(topicId: $activeTopicStats->topicId) : null; - case '': - $template = 'profile.index'; + $profileFieldValues = $profileFields->getFieldValues($userInfo); + $profileFieldInfos = $profileFieldInfos ?? $profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues); + $profileFieldFormats = $profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues); - if(!$viewingAsGuest) { - Template::set('profile_warnings', $msz->getWarnings()->getWarningsWithDefaultBacklog($userInfo)); + $profileFieldRawValues = []; + $profileFieldLinkValues = []; + $profileFieldDisplayValues = []; - if((!$isBanned || $canEdit)) { - $activeCategoryStats = forum_get_user_most_active_category_info($userInfo->getId()); - $activeCategoryInfo = empty($activeCategoryStats->forum_id) ? null : forum_get($activeCategoryStats->forum_id); + // using field infos as the basis for now, uses the correct ordering + foreach($profileFieldInfos as $fieldInfo) { + unset($fieldValue); - $activeTopicStats = forum_get_user_most_active_topic_info($userInfo->getId()); - $activeTopicInfo = empty($activeTopicStats->topic_id) ? null : forum_topic_get($activeTopicStats->topic_id); - - $profileFieldValues = $profileFields->getFieldValues($userInfo); - $profileFieldInfos = $profileFieldInfos ?? $profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues); - $profileFieldFormats = $profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues); - - $profileFieldRawValues = []; - $profileFieldLinkValues = []; - $profileFieldDisplayValues = []; - - // using field infos as the basis for now, uses the correct ordering - foreach($profileFieldInfos as $fieldInfo) { - unset($fieldValue); - - foreach($profileFieldValues as $fieldValueTest) - if($fieldValueTest->getFieldId() === $fieldInfo->getId()) { - $fieldValue = $fieldValueTest; - break; - } - - $fieldName = $fieldInfo->getName(); - - if(isset($fieldValue)) { - foreach($profileFieldFormats as $fieldFormatTest) - if($fieldFormatTest->getId() === $fieldValue->getFormatId()) { - $fieldFormat = $fieldFormatTest; - break; - } - - $profileFieldRawValues[$fieldName] = $fieldValue->getValue(); - $profileFieldDisplayValues[$fieldName] = $fieldFormat->formatDisplay($fieldValue->getValue()); - if($fieldFormat->hasLinkFormat()) - $profileFieldLinkValues[$fieldName] = $fieldFormat->formatLink($fieldValue->getValue()); - } + foreach($profileFieldValues as $fieldValueTest) + if($fieldValueTest->getFieldId() === $fieldInfo->getId()) { + $fieldValue = $fieldValueTest; + break; } - Template::set([ - 'profile_active_category_stats' => $activeCategoryStats, - 'profile_active_category_info' => $activeCategoryInfo, - 'profile_active_topic_stats' => $activeTopicStats, - 'profile_active_topic_info' => $activeTopicInfo, - 'profile_fields_infos' => $profileFieldInfos, - 'profile_fields_raw_values' => $profileFieldRawValues, - 'profile_fields_display_values' => $profileFieldDisplayValues, - 'profile_fields_link_values' => $profileFieldLinkValues, - ]); + $fieldName = $fieldInfo->getName(); + + if(isset($fieldValue)) { + foreach($profileFieldFormats as $fieldFormatTest) + if($fieldFormatTest->getId() === $fieldValue->getFormatId()) { + $fieldFormat = $fieldFormatTest; + break; + } + + $profileFieldRawValues[$fieldName] = $fieldValue->getValue(); + $profileFieldDisplayValues[$fieldName] = $fieldFormat->formatDisplay($fieldValue->getValue()); + if($fieldFormat->hasLinkFormat()) + $profileFieldLinkValues[$fieldName] = $fieldFormat->formatLink($fieldValue->getValue()); } } - break; + + Template::set([ + 'profile_active_category_stats' => $activeCategoryStats, + 'profile_active_category_info' => $activeCategoryInfo, + 'profile_active_topic_stats' => $activeTopicStats, + 'profile_active_topic_info' => $activeTopicInfo, + 'profile_fields_infos' => $profileFieldInfos, + 'profile_fields_raw_values' => $profileFieldRawValues, + 'profile_fields_display_values' => $profileFieldDisplayValues, + 'profile_fields_link_values' => $profileFieldLinkValues, + ]); + } } -if(!empty($template)) { - Template::render($template, [ - 'profile_viewer' => $viewerInfo, - 'profile_user' => $userInfo, - 'profile_colour' => $users->getUserColour($userInfo), - 'profile_stats' => $profileStats, - 'profile_mode' => $profileMode, - 'profile_notices' => $notices, - 'profile_can_edit' => $canEdit, - 'profile_is_editing' => $isEditing, - 'profile_is_banned' => $isBanned, - 'profile_is_guest' => $viewingAsGuest, - 'profile_is_deleted' => false, - 'profile_ban_info' => $activeBanInfo, - 'profile_avatar_info' => $avatarInfo, - 'profile_background_info' => $backgroundInfo, - ]); -} +Template::render('profile.index', [ + 'profile_viewer' => $viewerInfo, + 'profile_user' => $userInfo, + 'profile_colour' => $users->getUserColour($userInfo), + 'profile_stats' => $profileStats, + 'profile_mode' => $profileMode, + 'profile_notices' => $notices, + 'profile_can_edit' => $canEdit, + 'profile_is_editing' => $isEditing, + 'profile_is_banned' => $isBanned, + 'profile_is_guest' => $viewingAsGuest, + 'profile_is_deleted' => false, + 'profile_ban_info' => $activeBanInfo, + 'profile_avatar_info' => $avatarInfo, + 'profile_background_info' => $backgroundInfo, +]); diff --git a/public-legacy/search.php b/public-legacy/search.php index 1a6c52b..715a27e 100644 --- a/public-legacy/search.php +++ b/public-legacy/search.php @@ -1,7 +1,9 @@ isLoggedIn()) { @@ -11,35 +13,168 @@ if(!$msz->isLoggedIn()) { $searchQuery = !empty($_GET['q']) && is_string($_GET['q']) ? $_GET['q'] : ''; -if(!empty($searchQuery)) { - $forumTopics = forum_topic_listing_search($searchQuery, $msz->getActiveUser()->getId()); - $forumPosts = forum_post_search($searchQuery); +$searchQueryEvaluated = ['query' => []]; +Template::addFunction('search_merge_query', function($attrs) use (&$searchQueryEvaluated) { + $existing = []; - // this sure is an expansion - $news = $msz->getNews(); + if(!empty($attrs['type'])) + $existing[] = 'type:' . $attrs['type']; + elseif(!empty($searchQueryEvaluated['type'])) + $existing[] = 'type:' . $searchQueryEvaluated['type']; + + if(!empty($attrs['author'])) + $existing[] = 'author:' . $attrs['author']; + elseif(!empty($searchQueryEvaluated['author'])) + $existing[] = 'author:' . $searchQueryEvaluated['author']->getName(); + + if(!empty($attrs['after'])) + $existing[] = 'after:' . $attrs['after']; + elseif(!empty($searchQueryEvaluated['after'])) + $existing[] = 'after:' . $searchQueryEvaluated['after']; + + $existing = array_merge($existing, array_unique(array_merge( + $searchQueryEvaluated['query'], + empty($attrs['query']) ? [] : explode(' ', $attrs['query']) + ))); + + return rawurlencode(implode(' ', $existing)); +}); +if(!empty($searchQuery)) { $users = $msz->getUsers(); + $forum = $msz->getForum(); + $news = $msz->getNews(); $comments = $msz->getComments(); + + $userInfos = []; + $userColours = []; + + $searchQueryAttributes = ['type', 'author', 'after']; + $searchQueryParts = explode(' ', $searchQuery); + foreach($searchQueryParts as $queryPart) { + $queryPart = trim($queryPart); + if($queryPart === '') + continue; + + $colonIndex = strpos($queryPart, ':'); + if($colonIndex !== false && $colonIndex > 0) { + $attrName = substr($queryPart, 0, $colonIndex); + if(in_array($attrName, $searchQueryAttributes)) { + $attrValue = substr($queryPart, $colonIndex + 1); + $searchQueryEvaluated[$attrName] = $attrValue; + continue; + } + } + + $searchQueryEvaluated['query'][] = $queryPart; + } + + $searchQueryEvaluated['query_string'] = implode(' ', $searchQueryEvaluated['query']); + + if(!empty($searchQueryEvaluated['author'])) + try { + $searchQueryEvaluated['author'] = $users->getUser($searchQueryEvaluated['author'], 'search'); + } catch(RuntimeException $ex) { + unset($searchQueryEvaluated['author']); + } + + if(empty($searchQueryEvaluated['type']) || str_starts_with($searchQueryEvaluated['type'], 'forum')) { + $currentUser = $msz->getActiveUser(); + $currentUserId = $currentUser === null ? 0 : (int)$currentUser->getId(); + + $forumCategoryIds = XArray::where( + $forum->getCategories(hidden: false), + fn($categoryInfo) => $categoryInfo->mayHaveTopics() && forum_perms_check_user(MSZ_FORUM_PERMS_GENERAL, $categoryInfo->getId(), $currentUserId, MSZ_FORUM_PERM_VIEW_FORUM) + ); + + $forumTopicInfos = $forum->getTopics(categoryInfo: $forumCategoryIds, deleted: false, searchQuery: $searchQueryEvaluated); + $forumTopics = []; + + foreach($forumTopicInfos as $topicInfo) { + $forumTopics[] = $topic = new stdClass; + $topic->info = $topicInfo; + $topic->unread = $forum->checkTopicUnread($topicInfo, $currentUser); + $topic->participated = $forum->checkTopicParticipated($topicInfo, $currentUser); + $topic->lastPost = new stdClass; + + if($topicInfo->hasUserId()) { + $lastTopicUserId = $topicInfo->getUserId(); + if(!array_key_exists($lastTopicUserId, $userInfos)) { + $userInfo = $users->getUser($lastTopicUserId, 'id'); + $userInfos[$lastTopicUserId] = $userInfo; + $userColours[$lastTopicUserId] = $users->getUserColour($userInfo); + } + + $topic->user = $userInfos[$lastTopicUserId]; + $topic->colour = $userColours[$lastTopicUserId]; + } + + try { + $topic->lastPost->info = $lastPostInfo = $forum->getPost( + topicInfo: $topicInfo, + getLast: true, + deleted: $topicInfo->isDeleted() ? null : false, + ); + + if($lastPostInfo->hasUserId()) { + $lastPostUserId = $lastPostInfo->getUserId(); + if(!array_key_exists($lastPostUserId, $userInfos)) { + $userInfo = $users->getUser($lastPostUserId, 'id'); + $userInfos[$lastPostUserId] = $userInfo; + $userColours[$lastPostUserId] = $users->getUserColour($userInfo); + } + + $topic->lastPost->user = $userInfos[$lastPostUserId]; + $topic->lastPost->colour = $userColours[$lastPostUserId]; + } + } catch(RuntimeException $ex) {} + } + + $forumPostInfos = $forum->getPosts(categoryInfo: $forumCategoryIds, deleted: false, searchQuery: $searchQueryEvaluated); + $forumPosts = []; + + foreach($forumPostInfos as $postInfo) { + $forumPosts[] = $post = new stdClass; + $post->info = $postInfo; + + if($postInfo->hasUserId()) { + $postUserId = $postInfo->getUserId(); + if(!array_key_exists($postUserId, $userInfos)) { + $userInfo = $users->getUser($postUserId, 'id'); + $userInfos[$postUserId] = $userInfo; + $userColours[$postUserId] = $users->getUserColour($userInfo); + $userPostsCounts[$postUserId] = $forum->countPosts(userInfo: $userInfo, deleted: false); + } + + $post->user = $userInfos[$postUserId]; + $post->colour = $userColours[$postUserId]; + $post->postsCount = null; + } + + // can't be bothered sorry + $post->isOriginalPost = false; + $post->isOriginalPoster = false; + } + } + $newsPosts = []; - $newsPostInfos = $news->getPosts(searchQuery: $searchQuery); - $newsUserInfos = []; - $newsUserColours = []; + $newsPostInfos = empty($searchQueryEvaluated['type']) || $searchQueryEvaluated['type'] === 'news' ? $news->getPosts(searchQuery: $searchQuery) : []; $newsCategoryInfos = []; foreach($newsPostInfos as $postInfo) { $userId = $postInfo->getUserId(); $categoryId = $postInfo->getCategoryId(); - if(array_key_exists($userId, $newsUserInfos)) { - $userInfo = $newsUserInfos[$userId]; + if(array_key_exists($userId, $userInfos)) { + $userInfo = $userInfos[$userId]; } else { try { $userInfo = $users->getUser($userId, 'id'); - $newsUserColours[$userId] = $users->getUserColour($userInfo); + $userColours[$userId] = $users->getUserColour($userInfo); } catch(RuntimeException $ex) { $userInfo = null; } - $newsUserInfos[$userId] = $userInfo; + $userInfos[$userId] = $userInfo; } if(array_key_exists($categoryId, $newsCategoryInfos)) @@ -54,38 +189,40 @@ if(!empty($searchQuery)) { 'post' => $postInfo, 'category' => $categoryInfo, 'user' => $userInfo, - 'user_colour' => $newsUserColours[$userId] ?? \Index\Colour\Colour::none(), + 'user_colour' => $userColours[$userId] ?? \Index\Colour\Colour::none(), 'comments_count' => $commentsCount, ]; } - $findUsers = DB::prepare(' - SELECT u.`user_id`, u.`username`, u.`user_country`, - u.`user_created`, u.`user_active`, r.`role_id`, - COALESCE(u.`user_title`, r.`role_title`) AS `user_title`, - COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour`, - ( - SELECT COUNT(`topic_id`) - FROM `msz_forum_topics` - WHERE `user_id` = u.`user_id` - AND `topic_deleted` IS NULL - ) AS `user_count_topics`, - ( - SELECT COUNT(`post_Id`) - FROM `msz_forum_posts` - WHERE `user_id` = u.`user_id` - AND `post_deleted` IS NULL - ) AS `user_count_posts` - FROM `msz_users` AS u - LEFT JOIN `msz_roles` AS r - ON r.`role_id` = u.`display_role` - LEFT JOIN `msz_users_roles` AS ur - ON ur.`user_id` = u.`user_id` - WHERE u.`username` LIKE CONCAT("%%", :query, "%%") - GROUP BY u.`user_id` - '); - $findUsers->bind('query', $searchQuery); - $userList = $findUsers->fetchAll(); + if(empty($searchQueryEvaluated['type']) || $searchQueryEvaluated['type'] === 'member') { + $findUsers = DB::prepare(' + SELECT u.`user_id`, u.`username`, u.`user_country`, + u.`user_created`, u.`user_active`, r.`role_id`, + COALESCE(u.`user_title`, r.`role_title`) AS `user_title`, + COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour`, + ( + SELECT COUNT(`topic_id`) + FROM `msz_forum_topics` + WHERE `user_id` = u.`user_id` + AND `topic_deleted` IS NULL + ) AS `user_count_topics`, + ( + SELECT COUNT(`post_Id`) + FROM `msz_forum_posts` + WHERE `user_id` = u.`user_id` + AND `post_deleted` IS NULL + ) AS `user_count_posts` + FROM `msz_users` AS u + LEFT JOIN `msz_roles` AS r + ON r.`role_id` = u.`display_role` + LEFT JOIN `msz_users_roles` AS ur + ON ur.`user_id` = u.`user_id` + WHERE u.`username` LIKE CONCAT("%%", :query, "%%") + GROUP BY u.`user_id` + '); + $findUsers->bind('query', $searchQuery); + $userList = $findUsers->fetchAll(); + } } Template::render('home.search', [ diff --git a/src/Forum/Forum.php b/src/Forum/Forum.php new file mode 100644 index 0000000..590c20b --- /dev/null +++ b/src/Forum/Forum.php @@ -0,0 +1,1558 @@ +dbConn = $dbConn; + $this->cache = new DbStatementCache($dbConn); + } + + public static function convertCategoryListToTree( + array $catInfos, + ForumCategoryInfo|string|null $parentInfo = null, + ?Colour $colour = null + ): array { + $colour ??= Colour::none(); + $tree = []; + $predicate = $parentInfo + ? fn($catInfo) => $catInfo->isDirectChildOf($parentInfo) + : fn($catInfo) => !$catInfo->hasParent(); + + foreach($catInfos as $catInfo) { + if(!$predicate($catInfo)) + continue; + + $tree[$catInfo->getId()] = $item = new stdClass; + $item->info = $catInfo; + $item->colour = $catInfo->hasColour() ? $catInfo->getColour() : $colour; + $item->children = self::convertCategoryListToTree($catInfos, $catInfo, $item->colour); + $item->childIds = []; + foreach($item->children as $child) { + $item->childIds[] = $child->info->getId(); + $item->childIds += $child->childIds; + } + } + + return $tree; + } + + public function countCategories( + ForumCategoryInfo|string|null|false $parentInfo = false, + string|int|null $type = null, + ?bool $hidden = null + ): array { + if($parentInfo instanceof ForumCategoryInfo) + $parentInfo = $parentInfo->getId(); + + $hasParentInfo = $parentInfo !== false; + $hasType = $type !== null; + $hasHidden = $hidden !== null; + + $args = 0; + $query = 'SELECT COUNT(*) FROM msz_forum_categories'; + if($hasParentInfo) { + ++$args; + $isRootParent = $parentInfo === null; + if($isRootParent) { // make a migration that makes the field DEFAULT NULL and update all 0s to NULL + $query .= 'WHERE (forum_parent IS NULL OR forum_parent = 0)'; + } else { + $query .= 'WHERE forum_parent = ?'; + } + } + if($hasType) { + if(is_string($type)) { + if(!array_key_exists($type, ForumCategoryInfo::TYPE_ALIASES)) + throw new InvalidArgumentException('$type is not a valid alias.'); + $type = ForumCategoryInfo::TYPE_ALIASES[$type]; + } + + $query .= sprintf(' %s forum_type = ?', ++$args > 1 ? 'AND' : 'WHERE'); + } + if($hasHidden) + $query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '='); + + $args = 0; + $stmt = $this->cache->get($query); + if($hasParentInfo && !$isRootParent) + $stmt->addParameter(++$args, $parentInfo->getId()); + if($hasType) + $stmt->addParameter(++$args, $type); + $stmt->execute(); + + $result = $stmt->getResult(); + return $result->next() ? $result->getInteger(0) : 0; + } + + public function getCategories( + ForumCategoryInfo|string|null|false $parentInfo = false, + string|int|null $type = null, + ?bool $hidden = null, + bool $asTree = false, + ?Pagination $pagination = null + ): array { + $hasParentInfo = $parentInfo !== false; + $hasType = $type !== null; + $hasHidden = $hidden !== null; + $hasPagination = $pagination !== null; + + if($hasParentInfo && $asTree) + throw new InvalidArgumentException('$asTree can only be used with $parentInfo set to false.'); + + $args = 0; + $query = 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories'; + if($hasParentInfo) { + ++$args; + $isRootParent = $parentInfo === null; + if($isRootParent) { // make a migration that makes the field DEFAULT NULL and update all 0s to NULL + $query .= ' WHERE (forum_parent IS NULL OR forum_parent = 0)'; + } else { + $query .= ' WHERE forum_parent = ?'; + } + } + if($hasType) { + if(is_string($type)) { + if(!array_key_exists($type, ForumCategoryInfo::TYPE_ALIASES)) + throw new InvalidArgumentException('$type is not a valid alias.'); + $type = ForumCategoryInfo::TYPE_ALIASES[$type]; + } + + $query .= sprintf(' %s forum_type = ?', ++$args > 1 ? 'AND' : 'WHERE'); + } + if($hasHidden) + $query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '='); + $query .= ' ORDER BY forum_parent, forum_type <> 1, forum_order'; + if($hasPagination) + $query .= ' LIMIT ? OFFSET ?'; + + $args = 0; + $stmt = $this->cache->get($query); + if($hasParentInfo && !$isRootParent) { + if($parentInfo instanceof ForumCategoryInfo) + $stmt->addParameter(++$args, $parentInfo->getId()); + else + $stmt->addParameter(++$args, $parentInfo); + } + if($hasType) + $stmt->addParameter(++$args, $type); + if($hasPagination) { + $stmt->addParameter(++$args, $pagination->getRange()); + $stmt->addParameter(++$args, $pagination->getOffset()); + } + $stmt->execute(); + + $result = $stmt->getResult(); + $cats = []; + + while($result->next()) + $cats[] = new ForumCategoryInfo($result); + + if($asTree) + $cats = self::convertCategoryListToTree($cats); + + return $cats; + } + + public function getCategory( + ?string $categoryId = null, + ForumTopicInfo|string|null $topicInfo = null, + ForumPostInfo|string|null $postInfo = null + ): ForumCategoryInfo { + $hasCategoryId = $categoryId !== null; + $hasTopicInfo = $topicInfo !== null; + $hasPostInfo = $postInfo !== null; + + if(!$hasCategoryId && !$hasTopicInfo && !$hasPostInfo) + throw new InvalidArgumentException('You must specify an argument.'); + if(($hasCategoryId && ($hasTopicInfo || $hasPostInfo)) + || ($hasTopicInfo && ($hasCategoryId || $hasPostInfo)) + || ($hasPostInfo && ($hasCategoryId || $hasTopicInfo))) + throw new InvalidArgumentException('Only one argument may be specified.'); + + $value = null; + $query = 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories'; + if($hasCategoryId) { + $query .= ' WHERE forum_id = ?'; + $value = $categoryId; + } + if($hasTopicInfo) { + if($topicInfo instanceof ForumTopicInfo) { + $query .= ' WHERE forum_id = ?'; + $value = $topicInfo->getCategoryId(); + } else { + $query .= ' WHERE forum_id = (SELECT forum_id FROM msz_forum_topics WHERE topic_id = ?)'; + $value = $topicInfo; + } + } + if($hasPostInfo) { + if($postInfo instanceof ForumPostInfo) { + $query .= ' WHERE forum_id = ?'; + $value = $postInfo->getCategoryId(); + } else { + $query .= ' WHERE forum_id = (SELECT forum_id FROM msz_forum_posts WHERE post_id = ?)'; + $value = $postInfo; + } + } + + $stmt = $this->cache->get($query); + $stmt->addParameter(1, $value); + $stmt->execute(); + + $result = $stmt->getResult(); + if(!$result->next()) + throw new RuntimeException('Forum category info not found.'); + + return new ForumCategoryInfo($result); + } + + public function updateCategory( + ForumCategoryInfo|string $categoryInfo + ): void { + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + } + + public function deleteCategory(ForumCategoryInfo|string $categoryInfo): void { + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + } + + public function incrementCategoryClicks(ForumCategoryInfo|string $categoryInfo): void { + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + + // previous implementation also WHERE'd for forum_type = link but i don't think there's any other way to get here anyhow + $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_link_clicks = forum_link_clicks + 1 WHERE forum_id = ? AND forum_link_clicks IS NOT NULL'); + $stmt->addParameter(1, $categoryInfo); + $stmt->execute(); + } + + public function incrementCategoryTopics(ForumCategoryInfo|string $categoryInfo): void { + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + + $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_topics = forum_count_topics + 1 WHERE forum_id = ?'); + $stmt->addParameter(1, $categoryInfo); + $stmt->execute(); + } + + public function incrementCategoryPosts(ForumCategoryInfo|string $categoryInfo): void { + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + + $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_posts = forum_count_posts + 1 WHERE forum_id = ?'); + $stmt->addParameter(1, $categoryInfo); + $stmt->execute(); + } + + public function getCategoryAncestry( + ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo + ): array { + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo) + $categoryInfo = $categoryInfo->getCategoryId(); + + $query = 'WITH RECURSIVE msz_cte_ancestry AS (' + . 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories WHERE forum_id = ?' + . ' UNION ALL' + . ' SELECT fc.forum_id, fc.forum_order, fc.forum_parent, fc.forum_name, fc.forum_type, fc.forum_description, fc.forum_icon, fc.forum_colour, fc.forum_link, fc.forum_link_clicks, UNIX_TIMESTAMP(fc.forum_created), fc.forum_archived, fc.forum_hidden, fc.forum_count_topics, fc.forum_count_posts FROM msz_forum_categories AS fc JOIN msz_cte_ancestry AS ca ON fc.forum_id = ca.forum_parent' + . ') SELECT * FROM msz_cte_ancestry'; + + $stmt = $this->cache->get($query); + $stmt->addParameter(1, $categoryInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + $cats = []; + + while($result->next()) + $cats[] = new ForumCategoryInfo($result); + + return $cats; + } + + public function getCategoryChildren( + ForumCategoryInfo|string $parentInfo, + bool $includeSelf = false, + ?bool $hidden = null, + bool $asTree = false + ): array { + if($parentInfo instanceof ForumCategoryInfo) + $parentInfo = $parentInfo->getId(); + + $hasHidden = $hidden !== null; + + $query = 'WITH RECURSIVE msz_cte_children AS (' + . 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories WHERE forum_id = ?' + . ' UNION ALL' + . ' SELECT fc.forum_id, fc.forum_order, fc.forum_parent, fc.forum_name, fc.forum_type, fc.forum_description, fc.forum_icon, fc.forum_colour, fc.forum_link, fc.forum_link_clicks, UNIX_TIMESTAMP(fc.forum_created), fc.forum_archived, fc.forum_hidden, fc.forum_count_topics, fc.forum_count_posts FROM msz_forum_categories AS fc JOIN msz_cte_children AS cc ON fc.forum_parent = cc.forum_id' + . ') SELECT * FROM msz_cte_children'; + + $args = 0; + if(!$includeSelf) { + ++$args; + $query .= ' WHERE forum_id <> ?'; + } + if($hasHidden) + $query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '='); + $query .= ' ORDER BY forum_parent, forum_order'; + + $args = 0; + $stmt = $this->cache->get($query); + $stmt->addParameter(++$args, $parentInfo); + if(!$includeSelf) + $stmt->addParameter(++$args, $parentInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + $cats = []; + + while($result->next()) + $cats[] = new ForumCategoryInfo($result); + + if($asTree) + $cats = self::convertCategoryListToTree($cats, $parentInfo); + + return $cats; + } + + public function checkCategoryUnread( + ForumCategoryInfo|string|array $categoryInfos, + UserInfo|string|null $userInfo + ): bool { + if($userInfo === null) + return false; + + if(!is_array($categoryInfos)) + $categoryInfos = [$categoryInfos]; + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + $args = 0; + $stmt = $this->cache->get(sprintf( + 'SELECT COUNT(*) FROM msz_forum_topics AS ft LEFT JOIN msz_forum_topics_track AS ftt ON ftt.topic_id = ft.topic_id AND ftt.user_id = ? WHERE ft.forum_id IN (%s) AND ft.topic_deleted IS NULL AND ft.topic_bumped >= NOW() - INTERVAL 1 MONTH AND (ftt.track_last_read IS NULL OR ftt.track_last_read < ft.topic_bumped)', + DbTools::prepareListString($categoryInfos) + )); + $stmt->addParameter(++$args, $userInfo); + foreach($categoryInfos as $categoryInfo) { + if($categoryInfo instanceof ForumCategoryInfo) + $stmt->addParameter(++$args, $categoryInfo->getId()); + elseif(is_string($categoryInfo) || is_int($categoryInfo)) + $stmt->addParameter(++$args, $categoryInfo); + else + throw new InvalidArgumentException('Invalid item in $categoryInfos.'); + } + $stmt->execute(); + + $result = $stmt->getResult(); + return $result->next() && $result->getInteger(0) > 0; + } + + public function updateUserReadCategory( + UserInfo|string|null $userInfo, + ForumCategoryInfo|string $categoryInfo + ): void { + if($userInfo === null) + return; + + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + if($categoryInfo instanceof $categoryInfo) + $categoryInfo = $categoryInfo->getId(); + + $stmt = $this->cache->get('REPLACE INTO msz_forum_topics_track (user_id, topic_id, forum_id, track_last_read) SELECT ?, topic_id, forum_id, NOW() FROM msz_forum_topics WHERE forum_id = ? AND topic_bumped >= NOW() - INTERVAL 1 MONTH'); + $stmt->addParameter(1, $userInfo); + $stmt->addParameter(2, $categoryInfo); + $stmt->execute(); + } + + public function getCategoryColour( + ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo + ): Colour { + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo) + $categoryInfo = $categoryInfo->getCategoryId(); + + $query = 'WITH RECURSIVE msz_cte_colours AS (' + . 'SELECT forum_id, forum_parent, forum_colour FROM msz_forum_categories WHERE forum_id = ?' + . ' UNION ALL' + . ' SELECT fc.forum_id, fc.forum_parent, fc.forum_colour FROM msz_forum_categories AS fc JOIN msz_cte_colours AS cc ON fc.forum_id = cc.forum_parent' + . ') SELECT forum_colour FROM msz_cte_colours WHERE forum_colour IS NOT NULL'; + + $stmt = $this->cache->get($query); + $stmt->addParameter(1, $categoryInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + return $result->next() ? Colour::fromMisuzu($result->getInteger(0)) : Colour::none(); + } + + public function getMostActiveCategoryInfo( + UserInfo|string $userInfo, + array $exceptCategoryInfos = [], + array $exceptTopicInfos = [], + ?bool $deleted = null + ): object { + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + $hasExceptCategoryInfos = !empty($exceptCategoryInfos); + $hasExceptTopicInfos = !empty($exceptTopicInfos); + $hasDeleted = $deleted !== null; + + $query = 'SELECT forum_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = ?'; + if($hasDeleted) + $query .= sprintf(' AND post_deleted %s NULL', $deleted ? 'IS NOT' : 'IS'); + if($hasExceptCategoryInfos) + $query .= sprintf(' AND forum_id NOT IN (%s)', DbTools::prepareListString($exceptCategoryInfos)); + if($hasExceptTopicInfos) + $query .= sprintf(' AND topic_id NOT IN (%s)', DbTools::prepareListString($exceptTopicInfos)); + $query .= ' GROUP BY forum_id ORDER BY post_count DESC LIMIT 1'; + + $args = 0; + $stmt = $this->cache->get($query); + $stmt->addParameter(++$args, $userInfo); + foreach($exceptCategoryInfos as $categoryInfo) { + if($categoryInfo instanceof ForumCategoryInfo) + $stmt->addParameter(++$args, $categoryInfo->getId()); + elseif(is_string($categoryInfo) || is_int($categoryInfo)) + $stmt->addParameter(++$args, (string)$categoryInfo); + else + throw new InvalidArgumentException('$exceptCategoryInfos may only contain string ids or instances of ForumCategoryInfo.'); + } + foreach($exceptTopicInfos as $topicInfo) { + if($topicInfo instanceof ForumTopicInfo) + $stmt->addParameter(++$args, $topicInfo->getId()); + elseif(is_string($topicInfo) || is_int($topicInfo)) + $stmt->addParameter(++$args, (string)$topicInfo); + else + throw new InvalidArgumentException('$exceptTopicInfos may only contain string ids or instances of ForumTopicInfo.'); + } + $stmt->execute(); + + $result = $stmt->getResult(); + $info = new stdClass; + $info->success = $result->next(); + if($info->success) { + $info->categoryId = $result->getString(0); + $info->postCount = $result->getInteger(1); + } + + return $info; + } + + public function syncForumCounters( + ForumCategoryInfo|string|null $categoryInfo = null, + bool $updateCounters = true + ): object { + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + elseif($categoryInfo === null) + $categoryInfo = '0'; + + $counters = new stdClass; + + $stmt = $this->cache->get('SELECT ? AS target_category_id, (SELECT COUNT(*) FROM msz_forum_topics WHERE forum_id = target_category_id AND topic_deleted IS NULL) AS count_topics, (SELECT COUNT(*) FROM msz_forum_posts WHERE forum_id = target_category_id AND post_deleted IS NULL) AS count_posts'); + $stmt->addParameter(1, $categoryInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + if(!$result->next()) + throw new RuntimeException('Failed to fetch forum category counters.'); + + $counters->topics = $result->getInteger(1); + $counters->posts = $result->getInteger(2); + + $stmt = $this->cache->get('SELECT forum_id FROM msz_forum_categories WHERE forum_parent = ?'); + $stmt->addParameter(1, $categoryInfo); + $stmt->execute(); + + $children = []; + $result = $stmt->getResult(); + while($result->next()) + $children[] = $result->getString(0); + + foreach($children as $childId) { + $childCounters = $this->syncForumCounters($childId, $updateCounters); + $counters->topics += $childCounters->topics; + $counters->posts += $childCounters->posts; + } + + if($updateCounters && $categoryInfo !== '0') { + $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_topics = ?, forum_count_posts = ? WHERE forum_id = ?'); + $stmt->addParameter(1, $counters->topics); + $stmt->addParameter(2, $counters->posts); + $stmt->addParameter(3, $categoryInfo); + $stmt->execute(); + } + + return $counters; + } + + public function countTopics( + ForumCategoryInfo|string|array|null $categoryInfo = null, + UserInfo|string|null $userInfo = null, + ?bool $global = null, + ?bool $deleted = null + ): int { + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + $hasCategoryInfo = $categoryInfo !== null; + $hasUserInfo = $userInfo !== null; + $hasGlobal = $global !== null; + $hasDeleted = $deleted !== null; + + $args = 0; + $query = 'SELECT COUNT(*) FROM msz_forum_topics'; + if($hasCategoryInfo || $hasGlobal) { + ++$args; + + // wow this sucks + $hasGlobalAndCategory = $hasCategoryInfo && $hasGlobal; + $query .= ' WHERE '; + if($hasGlobalAndCategory) + $query .= '('; + + if($hasCategoryInfo) { + if(is_array($categoryInfo)) + $query .= sprintf('forum_id IN (%s)', DbTools::prepareListString($categoryInfo)); + else + $query .= 'forum_id = ?'; + } + + if($hasGlobalAndCategory) + $query .= ' OR '; + + if($hasGlobal) // not sure why you would ever set this to false, but consistency! + $query .= sprintf('topic_type %s %d', $global ? '=' : '<>', ForumTopicInfo::TYPE_GLOBAL); + + if($hasGlobalAndCategory) + $query .= ')'; + } + if($hasUserInfo) + $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasDeleted) + $query .= sprintf(' %s topic_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS'); + + $args = 0; + $stmt = $this->cache->get($query); + if($hasCategoryInfo) { + if(is_array($categoryInfo)) { + foreach($categoryInfo as $categoryInfoEntry) + $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->getId() : (string)$categoryInfoEntry); + } else + $stmt->addParameter(++$args, $categoryInfo); + } + if($hasUserInfo) + $stmt->addParameter(++$args, $userInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + return $result->next() ? $result->getInteger(0) : 0; + } + + public function getTopics( + ForumCategoryInfo|string|array|null $categoryInfo = null, + UserInfo|string|null $userInfo = null, + ?array $searchQuery = null, + ?bool $global = null, + ?bool $deleted = null, + ?Pagination $pagination = null + ): array { + // remove this hack when search server + $hasSearchQuery = $searchQuery !== null; + $hasAfterTopicId = false; + $doSearchOrder = false; + if($hasSearchQuery) { + if(!empty($searchQuery['type']) + && $searchQuery['type'] !== 'forum' + && $searchQuery['type'] !== 'forum:topic') + return []; + + $deleted = false; + $pagination = null; + $doSearchOrder = true; + + if(!empty($searchQuery['author'])) + $userInfo = $searchQuery['author']; + + if(!empty($searchQuery['after'])) { + $hasAfterTopicId = true; + $afterTopicId = $searchQuery['after']; + } + + $searchQuery = $searchQuery['query_string']; + $hasSearchQuery = !empty($searchQuery); + } + + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + $hasCategoryInfo = $categoryInfo !== null; + $hasUserInfo = $userInfo !== null; + $hasGlobal = $global !== null; + $hasDeleted = $deleted !== null; + $hasPagination = $pagination !== null; + + $args = 0; + $query = 'SELECT topic_id, forum_id, user_id, topic_type, topic_title, topic_count_views, UNIX_TIMESTAMP(topic_created), UNIX_TIMESTAMP(topic_bumped), UNIX_TIMESTAMP(topic_deleted), UNIX_TIMESTAMP(topic_locked), (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NULL) AS topic_count_posts, (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NOT NULL) AS topic_count_posts_deleted FROM msz_forum_topics AS ft'; + if($hasCategoryInfo || $hasGlobal) { + ++$args; + + // wow this sucks + $hasGlobalAndCategory = $hasCategoryInfo && $hasGlobal; + $query .= ' WHERE '; + if($hasGlobalAndCategory) + $query .= '('; + + if($hasCategoryInfo) { + if(is_array($categoryInfo)) + $query .= sprintf('forum_id IN (%s)', DbTools::prepareListString($categoryInfo)); + else + $query .= 'forum_id = ?'; + } + + if($hasGlobalAndCategory) + $query .= ' OR '; + + if($hasGlobal) // not sure why you would ever set this to false, but consistency! + $query .= sprintf('topic_type %s %d', $global ? '=' : '<>', ForumTopicInfo::TYPE_GLOBAL); + + if($hasGlobalAndCategory) + $query .= ')'; + } + if($hasUserInfo) + $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasAfterTopicId) + $query .= sprintf(' %s topic_id > ?', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasSearchQuery) + $query .= sprintf(' %s MATCH(topic_title) AGAINST (? IN NATURAL LANGUAGE MODE)', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasDeleted) + $query .= sprintf(' %s topic_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS'); + if($doSearchOrder) { + $query .= ' ORDER BY topic_id ASC LIMIT 20'; + } else { + $query .= ' ORDER BY topic_type DESC, topic_bumped DESC'; + if($hasPagination) + $query .= ' LIMIT ? OFFSET ?'; + } + + $args = 0; + $stmt = $this->cache->get($query); + if($hasCategoryInfo) { + if(is_array($categoryInfo)) { + foreach($categoryInfo as $categoryInfoEntry) + $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->getId() : (string)$categoryInfoEntry); + } else + $stmt->addParameter(++$args, $categoryInfo); + } + if($hasUserInfo) + $stmt->addParameter(++$args, $userInfo); + if($hasAfterTopicId) + $stmt->addParameter(++$args, $afterTopicId); + if($hasSearchQuery) + $stmt->addParameter(++$args, $searchQuery); + if($hasPagination) { + $stmt->addParameter(++$args, $pagination->getRange()); + $stmt->addParameter(++$args, $pagination->getOffset()); + } + $stmt->execute(); + + $result = $stmt->getResult(); + $topics = []; + + while($result->next()) + $topics[] = new ForumTopicInfo($result); + + return $topics; + } + + public function getTopic( + ?string $topicId = null, + ForumPostInfo|string|null $postInfo = null + ): ForumTopicInfo { + $hasTopicId = $topicId !== null; + $hasPostInfo = $postInfo !== null; + + if(!$hasTopicId && !$hasPostInfo) + throw new InvalidArgumentException('At least one argument must be specified.'); + if($hasTopicId && $hasPostInfo) + throw new InvalidArgumentException('Only one argument may be specified.'); + + $value = null; + $query = 'SELECT topic_id, forum_id, user_id, topic_type, topic_title, topic_count_views, UNIX_TIMESTAMP(topic_created), UNIX_TIMESTAMP(topic_bumped), UNIX_TIMESTAMP(topic_deleted), UNIX_TIMESTAMP(topic_locked), (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NULL) AS topic_count_posts, (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NOT NULL) AS topic_count_posts_deleted FROM msz_forum_topics AS ft'; + if($hasTopicId) { + $query .= ' WHERE topic_id = ?'; + $value = $topicId; + } + if($hasPostInfo) { + if($postInfo instanceof ForumPostInfo) { + $query .= ' WHERE topic_id = ?'; + $value = $postInfo->getTopicId(); + } else { + $query .= ' WHERE topic_id = (SELECT topic_id FROM msz_forum_posts WHERE post_id = ?)'; + $value = $postInfo; + } + } + + $stmt = $this->cache->get($query); + $stmt->addParameter(1, $value); + $stmt->execute(); + + $result = $stmt->getResult(); + if(!$result->next()) + throw new RuntimeException('Forum topic not found.'); + + return new ForumTopicInfo($result); + } + + public function createTopic( + ForumCategoryInfo|string $categoryInfo, + UserInfo|string|null $userInfo, + string $title, + string|int $type = ForumTopicInfo::TYPE_DISCUSSION + ): ForumTopicInfo { + if(is_string($type)) { + if(!array_key_exists($type, ForumTopicInfo::TYPE_ALIASES)) + throw new InvalidArgumentException('$type is not a valid alias.'); + $type = ForumTopicInfo::TYPE_ALIASES[$type]; + } + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + $stmt = $this->cache->get('INSERT INTO msz_forum_topics (forum_id, user_id, topic_type, topic_title) VALUES (?, ?, ?, ?)'); + $stmt->addParameter(1, $categoryInfo); + $stmt->addParameter(2, $userInfo); + $stmt->addParameter(3, $type); + $stmt->addParameter(4, $title); + $stmt->execute(); + + return $this->getTopic(topicId: (string)$this->dbConn->getLastInsertId()); + } + + public function updateTopic( + ForumTopicInfo|string $topicInfo, + ?string $title = null, + string|int|null $type = null + ): void { + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + + $fields = []; + $values = []; + + if($title !== null) { + $fields[] = 'topic_title = ?'; + $values[] = $title; + } + + if($type !== null) { + if(is_string($type)) { + if(!array_key_exists($type, ForumTopicInfo::TYPE_ALIASES)) + throw new InvalidArgumentException('$type is not a valid type alias.'); + + $type = ForumTopicInfo::TYPE_ALIASES[$type]; + } + + $fields[] = 'topic_type = ?'; + $values[] = $type; + } + + if(empty($fields)) + return; + + $args = 0; + $stmt = $this->cache->get(sprintf('UPDATE msz_forum_topics SET %s WHERE topic_id = ?', implode(', ', $fields))); + foreach($values as $value) + $stmt->addParameter(++$args, $value); + $stmt->addParameter(++$args, $topicInfo); + $stmt->execute(); + } + + public function incrementTopicView(ForumTopicInfo|string $topicInfo): void { + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + + $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_count_views = topic_count_views + 1 WHERE topic_id = ?'); + $stmt->addParameter(1, $topicInfo); + $stmt->execute(); + } + + public function bumpTopic(ForumTopicInfo|string $topicInfo): void { + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + + $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_bumped = NOW() WHERE topic_id = ?'); + $stmt->addParameter(1, $topicInfo); + $stmt->execute(); + } + + public function lockTopic(ForumTopicInfo|string $topicInfo): void { + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + + $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_locked = NOW() WHERE topic_id = ?'); + $stmt->addParameter(1, $topicInfo); + $stmt->execute(); + } + + public function unlockTopic(ForumTopicInfo|string $topicInfo): void { + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + + $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_locked = NULL WHERE topic_id = ?'); + $stmt->addParameter(1, $topicInfo); + $stmt->execute(); + } + + public function deleteTopic(ForumTopicInfo|string $topicInfo): void { + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + + $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_deleted = COALESCE(topic_deleted, NOW()) WHERE topic_id = ? AND topic_deleted IS NULL'); + $stmt->addParameter(1, $topicInfo); + $stmt->execute(); + + $stmt = $this->cache->get('UPDATE msz_forum_posts AS fp SET post_deleted = (SELECT topic_deleted FROM msz_forum_topics WHERE topic_id = fp.topic_id) WHERE topic_id = ? AND post_deleted = NULL'); + $stmt->addParameter(1, $topicInfo); + $stmt->execute(); + } + + public function restoreTopic(ForumTopicInfo|string $topicInfo): void { + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + + $stmt = $this->cache->get('UPDATE msz_forum_posts AS fp SET post_deleted = NULL WHERE topic_id = ? AND post_deleted = (SELECT topic_deleted FROM msz_forum_topics WHERE topic_id = fp.topic_id)'); + $stmt->addParameter(1, $topicInfo); + $stmt->execute(); + + $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_deleted = NULL WHERE topic_id = ?'); + $stmt->addParameter(1, $topicInfo); + $stmt->execute(); + } + + public function nukeTopic(ForumTopicInfo|string $topicInfo): void { + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + + $stmt = $this->cache->get('DELETE FROM msz_forum_topics WHERE topic_id = ?'); + $stmt->addParameter(1, $topicInfo); + $stmt->execute(); + } + + public function checkTopicParticipated( + ForumTopicInfo|string $topicInfo, + UserInfo|string|null $userInfo + ): bool { + if($userInfo === null) + return false; + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + $stmt = $this->cache->get('SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ? AND user_id = ?'); + $stmt->addParameter(1, $topicInfo); + $stmt->addParameter(2, $userInfo); + $stmt->execute(); + $result = $stmt->getResult(); + + return $result->next() && $result->getInteger(0) > 0; + } + + public function checkTopicUnread( + ForumTopicInfo|string $topicInfo, + UserInfo|string|null $userInfo + ): bool { + if($userInfo === null) + return false; + + $topicInfoIsInstance = $topicInfo instanceof ForumTopicInfo; + if($topicInfoIsInstance && !$topicInfo->isActive()) + return false; + + $query = 'SELECT UNIX_TIMESTAMP(track_last_read) FROM msz_forum_topics_track AS ftt WHERE user_id = ? AND topic_id = ?'; + if(!$topicInfoIsInstance) + $query .= ' AND track_last_read = (SELECT topic_bumped FROM msz_forum_topics WHERE topic_id = ftt.topic_id AND topic_bumped >= NOW() - INTERVAL 1 MONTH)'; + + $stmt = $this->cache->get($query); + $stmt->addParameter(1, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo); + $stmt->addParameter(2, $topicInfoIsInstance ? $topicInfo->getId() : $topicInfo); + $stmt->execute(); + $result = $stmt->getResult(); + + // user has never read this topic, return unread + if(!$result->next()) + return true; + + return $result->getInteger(0) < $topicInfo->getBumpedTime(); + } + + public function getMostActiveTopicInfo( + UserInfo|string $userInfo, + array $exceptCategoryInfos = [], + array $exceptTopicInfos = [], + ?bool $deleted = null + ): object { + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + $hasExceptCategoryInfos = !empty($exceptCategoryInfos); + $hasExceptTopicInfos = !empty($exceptTopicInfos); + $hasDeleted = $deleted !== null; + + $query = 'SELECT topic_id, forum_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = ?'; + if($hasDeleted) + $query .= sprintf(' AND post_deleted %s NULL', $deleted ? 'IS NOT' : 'IS'); + if($hasExceptCategoryInfos) + $query .= sprintf(' AND forum_id NOT IN (%s)', DbTools::prepareListString($exceptCategoryInfos)); + if($hasExceptTopicInfos) + $query .= sprintf(' AND topic_id NOT IN (%s)', DbTools::prepareListString($exceptTopicInfos)); + $query .= ' GROUP BY topic_id ORDER BY post_count DESC LIMIT 1'; + + $args = 0; + $stmt = $this->cache->get($query); + $stmt->addParameter(++$args, $userInfo); + foreach($exceptCategoryInfos as $categoryInfo) { + if($categoryInfo instanceof ForumCategoryInfo) + $stmt->addParameter(++$args, $categoryInfo->getId()); + elseif(is_string($categoryInfo) || is_int($categoryInfo)) + $stmt->addParameter(++$args, (string)$categoryInfo); + else + throw new InvalidArgumentException('$exceptCategoryInfos may only contain string ids or instances of ForumCategoryInfo.'); + } + foreach($exceptTopicInfos as $topicInfo) { + if($topicInfo instanceof ForumTopicInfo) + $stmt->addParameter(++$args, $topicInfo->getId()); + elseif(is_string($topicInfo) || is_int($topicInfo)) + $stmt->addParameter(++$args, (string)$topicInfo); + else + throw new InvalidArgumentException('$exceptTopicInfos may only contain string ids or instances of ForumTopicInfo.'); + } + $stmt->execute(); + + $result = $stmt->getResult(); + $info = new stdClass; + $info->success = $result->next(); + if($info->success) { + $info->topicId = $result->getString(0); + $info->categoryId = $result->getString(1); + $info->postCount = $result->getInteger(2); + } + + return $info; + } + + public function checkUserHasReadTopic( + UserInfo|string|null $userInfo, + ForumTopicInfo|string $topicInfo + ): bool { + // this method is primarily used to check if we should increment the view count + // guests shouldn't increment it so we just + if($userInfo === null) + return true; + + $stmt = $this->cache->get('SELECT COUNT(*) FROM msz_forum_topics_track WHERE topic_id = ? AND user_id = ?'); + $stmt->addParameter(1, $topicInfo instanceof ForumTopicInfo ? $topicInfo->getId() : $topicInfo); + $stmt->addParameter(2, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo); + $stmt->execute(); + $result = $stmt->getResult(); + + return $result->next() && $result->getInteger(0) > 0; + } + + public function updateUserReadTopic( + UserInfo|string|null $userInfo, + ForumTopicInfo|string $topicInfo, + ForumCategoryInfo|string|null $categoryInfo = null + ): void { + if($userInfo === null) + return; + + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + if($topicInfo instanceof ForumTopicInfo) { + $categoryInfo = $topicInfo->getCategoryId(); + $topicInfo = $topicInfo->getId(); + } else { + if($categoryInfo === null) + throw new InvalidArgumentException('$categoryInfo must be specified if $topicInfo is not an instance of ForumTopicInfo.'); + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + } + + $stmt = $this->cache->get('REPLACE INTO msz_forum_topics_track (user_id, topic_id, forum_id, track_last_read) VALUES (?, ?, ?, NOW())'); + $stmt->addParameter(1, $userInfo); + $stmt->addParameter(2, $topicInfo); + $stmt->addParameter(3, $categoryInfo); + $stmt->execute(); + } + + public function countTopicRedirects( + UserInfo|string|null $userInfo = null + ): int { + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + $hasUserInfo = $userInfo !== null; + + $query = 'SELECT COUNT(*) FROM msz_forum_topics_redirects'; + if($hasUserInfo) + $query .= ' WHERE user_id = ?'; + + $stmt = $this->cache->get($query); + if($hasUserInfo) + $stmt->addParameter(1, $userInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + return $result->next() ? $result->getInteger(0) : 0; + } + + public function getTopicRedirects( + UserInfo|string|null $userInfo = null, + ?Pagination $pagination = null + ): array { + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + $hasUserInfo = $userInfo !== null; + $hasPagination = $pagination !== null; + + $query = 'SELECT topic_id, user_id, topic_redir_url, UNIX_TIMESTAMP(topic_redir_created) FROM msz_forum_topics_redirects'; + if($hasUserInfo) + $query .= ' WHERE user_id = ?'; + if($hasPagination) + $query .= ' LIMIT ? OFFSET ?'; + + $args = 0; + $stmt = $this->cache->get($query); + if($hasUserInfo) + $stmt->addParameter(++$args, $userInfo); + if($hasPagination) { + $stmt->addParameter(++$args, $pagination->getRange()); + $stmt->addParameter(++$args, $pagination->getOffset()); + } + $stmt->execute(); + + $result = $stmt->getResult(); + $redirs = []; + + while($result->next()) + $redirs[] = new ForumTopicRedirectInfo($result); + + return $redirs; + } + + public function hasTopicRedirect(ForumTopicInfo|string $topicInfo): bool { + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + + $stmt = $this->cache->get('SELECT COUNT(*) FROM msz_forum_topics_redirects WHERE topic_id = ?'); + $stmt->addParameter(1, $topicInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + if(!$result->next()) + throw new RuntimeException('Was unable to check if a redirect exists.'); + + return $result->getInteger(0) > 0; + } + + public function getTopicRedirect(ForumTopicInfo|string $topicInfo): ForumTopicRedirectInfo { + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + + $stmt = $this->cache->get('SELECT topic_id, user_id, topic_redir_url, UNIX_TIMESTAMP(topic_redir_created) FROM msz_forum_topics_redirects WHERE topic_id = ?'); + $stmt->addParameter(1, $topicInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + if(!$result->next()) + throw new RuntimeException('Could not find that forum topic redirect.'); + + return new ForumTopicRedirectInfo($result); + } + + public function createTopicRedirect( + ForumTopicInfo|string $topicInfo, + UserInfo|string|null $userInfo, + string $linkTarget + ): ForumTopicRedirectInfo { + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + $stmt = $this->cache->get('INSERT INTO msz_forum_topics_redirects (topic_id, user_id, topic_redir_url) VALUES (?, ?, ?)'); + $stmt->addParameter(1, $topicInfo); + $stmt->addParameter(2, $userInfo); + $stmt->addParameter(3, $linkTarget); + $stmt->execute(); + + return $this->getTopicRedirect($topicInfo); + } + + public function deleteTopicRedirect(ForumTopicRedirectInfo|ForumTopicInfo|string $topicInfo): void { + if($topicInfo instanceof ForumTopicRedirectInfo) + $topicInfo = $topicInfo->getTopicId(); + elseif($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + + $stmt = $this->cache->get('DELETE FROM msz_forum_topics_redirects WHERE topic_id = ?'); + $stmt->addParameter(1, $topicInfo); + $stmt->execute(); + } + + public function countPosts( + ForumCategoryInfo|string|null $categoryInfo = null, + ForumTopicInfo|string|null $topicInfo = null, + UserInfo|string|null $userInfo = null, + ForumPostInfo|string|null $upToPostInfo = null, + ?bool $deleted = null + ): int { + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + if($upToPostInfo instanceof ForumPostInfo) + $upToPostInfo = $upToPostInfo->getId(); + + $hasCategoryInfo = $categoryInfo !== null; + $hasTopicInfo = $topicInfo !== null; + $hasUserInfo = $userInfo !== null; + $hasUpToPostInfo = $upToPostInfo !== null; + $hasDeleted = $deleted !== null; + + $args = 0; + $query = 'SELECT COUNT(*) FROM msz_forum_posts'; + if($hasCategoryInfo) { + ++$args; + $query .= ' WHERE forum_id = ?'; + } + if($hasTopicInfo) + $query .= sprintf(' %s topic_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasUserInfo) + $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasUpToPostInfo) + $query .= sprintf(' %s post_id < ?', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasDeleted) + $query .= sprintf(' %s post_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS'); + + $args = 0; + $stmt = $this->cache->get($query); + if($hasCategoryInfo) + $stmt->addParameter(++$args, $categoryInfo); + if($hasTopicInfo) + $stmt->addParameter(++$args, $topicInfo); + if($hasUserInfo) + $stmt->addParameter(++$args, $userInfo); + if($hasUpToPostInfo) + $stmt->addParameter(++$args, $upToPostInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + return $result->next() ? $result->getInteger(0) : 0; + } + + public function getPosts( + ForumCategoryInfo|string|array|null $categoryInfo = null, + ForumTopicInfo|string|null $topicInfo = null, + UserInfo|string|null $userInfo = null, + ForumPostInfo|string|null $upToPostInfo = null, + ?array $searchQuery = null, + ?bool $deleted = null, + ?Pagination $pagination = null + ): array { + // remove this hack when search server + $hasSearchQuery = $searchQuery !== null; + $hasAfterPostId = false; + $doSearchOrder = false; + if($hasSearchQuery) { + if(!empty($searchQuery['type']) + && $searchQuery['type'] !== 'forum' + && $searchQuery['type'] !== 'forum:post') + return []; + + $deleted = false; + $pagination = null; + $doSearchOrder = true; + + if(!empty($searchQuery['author'])) + $userInfo = $searchQuery['author']; + + if(!empty($searchQuery['after'])) { + $hasAfterPostId = true; + $afterPostId = $searchQuery['after']; + } + + $searchQuery = $searchQuery['query_string']; + $hasSearchQuery = !empty($searchQuery); + } + + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + if($upToPostInfo instanceof ForumPostInfo) + $upToPostInfo = $upToPostInfo->getId(); + + $hasCategoryInfo = $categoryInfo !== null; + $hasTopicInfo = $topicInfo !== null; + $hasUserInfo = $userInfo !== null; + $hasUpToPostInfo = $upToPostInfo !== null; + $hasDeleted = $deleted !== null; + $hasPagination = $pagination !== null; + + $args = 0; + $query = 'SELECT post_id, topic_id, forum_id, user_id, INET6_NTOA(post_ip), post_text, post_parse, post_display_signature, UNIX_TIMESTAMP(post_created), UNIX_TIMESTAMP(post_edited), UNIX_TIMESTAMP(post_deleted) FROM msz_forum_posts'; + if($hasCategoryInfo) { + ++$args; + if(is_array($categoryInfo)) + $query .= sprintf(' WHERE forum_id IN (%s)', DbTools::prepareListString($categoryInfo)); + else + $query .= ' WHERE forum_id = ?'; + } + if($hasTopicInfo) + $query .= sprintf(' %s topic_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasUserInfo) + $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasUpToPostInfo) + $query .= sprintf(' %s post_id < ?', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasAfterPostId) + $query .= sprintf(' %s post_id > ?', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasSearchQuery) + $query .= sprintf(' %s MATCH(post_text) AGAINST (? IN NATURAL LANGUAGE MODE)', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasDeleted) + $query .= sprintf(' %s post_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS'); + if($doSearchOrder) { + $query .= ' ORDER BY post_id ASC LIMIT 20'; + } else { + $query .= ' ORDER BY post_id ASC'; + if($hasPagination) + $query .= ' LIMIT ? OFFSET ?'; + } + + $args = 0; + $stmt = $this->cache->get($query); + if($hasCategoryInfo) { + if(is_array($categoryInfo)) { + foreach($categoryInfo as $categoryInfoEntry) + $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->getId() : (string)$categoryInfoEntry); + } else + $stmt->addParameter(++$args, $categoryInfo); + } + if($hasTopicInfo) + $stmt->addParameter(++$args, $topicInfo); + if($hasUserInfo) + $stmt->addParameter(++$args, $userInfo); + if($hasUpToPostInfo) + $stmt->addParameter(++$args, $upToPostInfo); + if($hasAfterPostId) + $stmt->addParameter(++$args, $afterPostId); + if($hasSearchQuery) + $stmt->addParameter(++$args, $searchQuery); + if($hasPagination) { + $stmt->addParameter(++$args, $pagination->getRange()); + $stmt->addParameter(++$args, $pagination->getOffset()); + } + $stmt->execute(); + + $result = $stmt->getResult(); + $posts = []; + + while($result->next()) + $posts[] = new ForumPostInfo($result); + + return $posts; + } + + public function getPost( + ?string $postId = null, + ForumTopicInfo|string|null $topicInfo = null, + ForumCategoryInfo|string|array|null $categoryInfos = null, + UserInfo|string|null $userInfo = null, + bool $getLast = false, + ?bool $deleted = null + ): ForumPostInfo { + $hasPostId = $postId !== null; + $hasTopicInfo = $topicInfo !== null; + $hasCategoryInfos = $categoryInfos !== null; + $hasUserInfo = $userInfo !== null; + $hasDeleted = $deleted !== null; + + if(!$hasPostId && !$hasTopicInfo && !$hasCategoryInfos && !$hasUserInfo) + throw new InvalidArgumentException('At least one of the four first arguments must be specified.'); + + $values = []; + $query = 'SELECT post_id, topic_id, forum_id, user_id, INET6_NTOA(post_ip), post_text, post_parse, post_display_signature, UNIX_TIMESTAMP(post_created), UNIX_TIMESTAMP(post_edited), UNIX_TIMESTAMP(post_deleted) FROM msz_forum_posts'; + if($hasPostId) { + $query .= ' WHERE post_id = ?'; + $values[] = $postId; + } elseif($hasUserInfo) { + $query .= ' WHERE user_id = ?'; + $values[] = $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo; + $query .= sprintf(' ORDER BY post_id %s', $getLast ? 'DESC' : 'ASC'); + } elseif($hasTopicInfo) { + if($topicInfo instanceof ForumTopicInfo) + $topicInfo = $topicInfo->getId(); + + $query .= sprintf(' WHERE post_id = (SELECT %s(post_id) FROM msz_forum_posts WHERE topic_id = ?', $getLast ? 'MAX' : 'MIN'); + if($hasDeleted) + $query .= sprintf(' AND post_deleted %s NULL', $deleted ? 'IS NOT' : 'IS'); + $query .= ')'; + + $values[] = $topicInfo; + } elseif($hasCategoryInfos) { + if(!is_array($categoryInfos)) + $categoryInfos = [$categoryInfos]; + + $query .= sprintf( + ' WHERE post_id = (SELECT %s(post_id) FROM msz_forum_posts WHERE forum_id IN (%s)', + $getLast ? 'MAX' : 'MIN', + DbTools::prepareListString($categoryInfos) + ); + if($hasDeleted) + $query .= sprintf(' AND post_deleted %s NULL', $deleted ? 'IS NOT' : 'IS'); + $query .= ')'; + + foreach($categoryInfos as $categoryInfo) { + if($categoryInfo instanceof ForumCategoryInfo) + $values[] = $categoryInfo->getId(); + elseif(is_string($categoryInfo) || is_int($categoryInfo)) + $values[] = (string)$categoryInfo; + else + throw new InvalidArgumentException('$categoryInfos contains an invalid item.'); + } + } + + $args = 0; + $stmt = $this->cache->get($query); + foreach($values as $value) + $stmt->addParameter(++$args, $value); + $stmt->execute(); + + $result = $stmt->getResult(); + if(!$result->next()) + throw new RuntimeException('Forum post not found.'); + + return new ForumPostInfo($result); + } + + public function createPost( + ForumTopicInfo|string $topicInfo, + UserInfo|string|null $userInfo, + IPAddress|string $remoteAddr, + string $body, + int $bodyParser, + bool $displaySignature, + ForumCategoryInfo|string|null $categoryInfo = null + ): ForumPostInfo { + if($categoryInfo instanceof ForumCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + + if($topicInfo instanceof ForumTopicInfo) { + $categoryInfo ??= $topicInfo->getCategoryId(); + $topicInfo = $topicInfo->getId(); + } elseif($categoryInfo === null) + throw new InvalidArgumentException('$categoryInfo may only be null if $topicInfo is an instance of ForumTopicInfo.'); + + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + if($remoteAddr instanceof IPAddress) + $remoteAddr = (string)$remoteAddr; + + $stmt = $this->cache->get('INSERT INTO msz_forum_posts (topic_id, forum_id, user_id, post_ip, post_text, post_parse, post_display_signature) VALUES (?, ?, ?, INET6_ATON(?), ?, ?, ?)'); + $stmt->addParameter(1, $topicInfo); + $stmt->addParameter(2, $categoryInfo); + $stmt->addParameter(3, $userInfo); + $stmt->addParameter(4, $remoteAddr); + $stmt->addParameter(5, $body); + $stmt->addParameter(6, $bodyParser); + $stmt->addParameter(7, $displaySignature ? 1 : 0); + $stmt->execute(); + + return $this->getPost(postId: (string)$this->dbConn->getLastInsertId()); + } + + public function updatePost( + ForumPostInfo|string $postInfo, + IPAddress|string|null $remoteAddr = null, + ?string $body = null, + ?int $bodyParser = null, + ?bool $displaySignature = null, + bool $bumpEdited = true + ): void { + if($postInfo instanceof ForumPostInfo) + $postInfo = $postInfo->getId(); + + $fields = []; + $values = []; + + if($remoteAddr !== null) { + if($remoteAddr instanceof IPAddress) + $remoteAddr = (string)$remoteAddr; + + $fields[] = 'post_ip = INET6_ATON(?)'; + $values[] = $remoteAddr; + } + + if($body !== null) { + $fields[] = 'post_text = ?'; + $values[] = $body; + } + + if($bodyParser !== null) { + $fields[] = 'post_parse = ?'; + $values[] = $bodyParser; + } + + if($displaySignature !== null) { + $fields[] = 'post_display_signature = ?'; + $values[] = $displaySignature ? 1 : 0; + } + + if(empty($fields)) + return; + + if($bumpEdited) + $fields[] = 'post_edited = NOW()'; + + $args = 0; + $stmt = $this->cache->get(sprintf('UPDATE msz_forum_posts SET %s WHERE post_id = ?', implode(', ', $fields))); + foreach($values as $value) + $stmt->addParameter(++$args, $value); + $stmt->addParameter(++$args, $postInfo); + $stmt->execute(); + } + + public function deletePost(ForumPostInfo|string $postInfo): void { + if($postInfo instanceof ForumPostInfo) + $postInfo = $postInfo->getId(); + + $stmt = $this->cache->get('UPDATE msz_forum_posts SET post_deleted = COALESCE(post_deleted, NOW()) WHERE post_id = ?'); + $stmt->addParameter(1, $postInfo); + $stmt->execute(); + } + + public function restorePost(ForumPostInfo|string $postInfo): void { + if($postInfo instanceof ForumPostInfo) + $postInfo = $postInfo->getId(); + + $stmt = $this->cache->get('UPDATE msz_forum_posts SET post_deleted = NULL WHERE post_id = ?'); + $stmt->addParameter(1, $postInfo); + $stmt->execute(); + } + + public function nukePost(ForumPostInfo|string $postInfo): void { + if($postInfo instanceof ForumPostInfo) + $postInfo = $postInfo->getId(); + + $stmt = $this->cache->get('DELETE FROM msz_forum_posts WHERE post_id = ?'); + $stmt->addParameter(1, $postInfo); + $stmt->execute(); + } + + public function getUserLastPostCreatedTime(UserInfo|string $userInfo): int { + if($userInfo instanceof UserInfo) + $userInfo = $userInfo->getId(); + + // intentionally including deleted posts + $stmt = $this->cache->get('SELECT UNIX_TIMESTAMP(MAX(post_created)) FROM msz_forum_posts WHERE user_id = ?'); + $stmt->addParameter(1, $userInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + if(!$result->next()) + return 0; + + return $result->getInteger(0); + } + + public function getUserLastPostCreatedAt(UserInfo|string $userInfo): DateTime { + return DateTime::fromUnixTimeSeconds($this->getUserLastPostCreatedTime($userInfo)); + } + + public function generatePostRankings( + int $year = 0, + int $month = 0, + array $exceptCategoryInfos = [], + array $exceptTopicInfos = [] + ): array { + $hasYear = $year > 0; + $hasMonth = $hasYear && $month > 0; + $hasExcludedCategoryInfos = !empty($exceptCategoryInfos); + $hasExcludedTopicInfos = !empty($exceptTopicInfos); + + $query = 'SELECT user_id, COUNT(*) AS posts_count FROM msz_forum_posts WHERE post_deleted IS NULL'; + if($hasYear) + $query .= sprintf( + ' AND DATE(post_created) BETWEEN "%1$04d-%2$02d-01" AND "%1$04d-%3$02d-31"', + $year, + $hasMonth ? $month : 1, + $hasMonth ? $month : 12 + ); + if($hasExcludedCategoryInfos) + $query .= sprintf(' AND forum_id NOT IN (%s)', DbTools::prepareListString($exceptCategoryInfos)); + if($hasExcludedTopicInfos) + $query .= sprintf(' AND topic_id NOT IN (%s)', DbTools::prepareListString($exceptTopicInfos)); + $query .= ' GROUP BY user_id HAVING posts_count > 0 ORDER BY posts_count DESC'; + + $args = 0; + $stmt = $this->cache->get($query); + foreach($exceptCategoryInfos as $exceptCategoryInfo) + $stmt->addParameter(++$args, $exceptCategoryInfo instanceof ForumCategoryInfo ? $exceptCategoryInfo->getId() : $exceptCategoryInfo); + foreach($exceptTopicInfos as $exceptTopicInfo) + $stmt->addParameter(++$args, $exceptTopicInfo instanceof ForumTopicInfo ? $exceptTopicInfo->getId() : $exceptTopicInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + $rankings = []; + $rankNo = 0; + $lastPostsCount = PHP_INT_MAX; + + while($result->next()) { + $rankings[] = $ranking = new stdClass; + $ranking->userId = $result->getString(0); + $ranking->postsCount = $result->getInteger(1); + + if($lastPostsCount > $ranking->postsCount) { + ++$rankNo; + $lastPostsCount = $ranking->postsCount; + } + + $ranking->position = $rankNo; + } + + return $rankings; + } +} diff --git a/src/Forum/ForumCategoryInfo.php b/src/Forum/ForumCategoryInfo.php new file mode 100644 index 0000000..60437dc --- /dev/null +++ b/src/Forum/ForumCategoryInfo.php @@ -0,0 +1,194 @@ + self::TYPE_DISCUSSION, + 'listing' => self::TYPE_LISTING, + 'link' => self::TYPE_LINK, + ]; + + public const MAY_HAVE_CHILDREN = [ + self::TYPE_DISCUSSION, + self::TYPE_LISTING, + ]; + + public const MAY_HAVE_TOPICS = [ + self::TYPE_DISCUSSION, + ]; + + private string $id; + private int $order; + private ?string $parentId; + private string $name; + private int $type; + private ?string $desc; + private ?string $icon; + private ?int $colour; + private ?string $link; + private ?int $clicks; + private int $created; + private bool $archived; + private bool $hidden; + private int $topicsCount; + private int $postsCount; + + public function __construct(IDbResult $result) { + $this->id = (string)$result->getInteger(0); + $this->order = $result->getInteger(1); + $this->parentId = $result->isNull(2) ? null : (string)$result->getInteger(2); + $this->name = $result->getString(3); + $this->type = $result->getInteger(4); + $this->desc = $result->isNull(5) ? null : $result->getString(5); + $this->icon = $result->isNull(6) ? null : $result->getString(6); + $this->colour = $result->isNull(7) ? null : $result->getInteger(7); + $this->link = $result->isNull(8) ? null : $result->getString(8); + $this->clicks = $result->isNull(9) ? null : $result->getInteger(9); + $this->created = $result->getInteger(10); + $this->archived = $result->getInteger(11) !== 0; + $this->hidden = $result->getInteger(12) !== 0; + $this->topicsCount = $result->getInteger(13); + $this->postsCount = $result->getInteger(14); + } + + public function getId(): string { + return $this->id; + } + + public function getOrder(): int { + return $this->order; + } + + public function hasParent(): bool { + return $this->parentId !== null && $this->parentId !== '0'; + } + + public function getParentId(): ?string { + return $this->parentId; + } + + public function isDirectChildOf(ForumCategoryInfo|string $parentInfo): bool { + if($parentInfo instanceof ForumCategoryInfo) + $parentInfo = $parentInfo->getId(); + return $this->hasParent() && $this->getParentId() === $parentInfo; + } + + public function getName(): string { + return $this->name; + } + + public function getType(): int { + return $this->type; + } + + public function isDiscussion(): bool { + return $this->type === self::TYPE_DISCUSSION; + } + + public function isListing(): bool { + return $this->type === self::TYPE_LISTING; + } + + public function isLink(): bool { + return $this->type === self::TYPE_LINK; + } + + public function mayHaveChildren(): bool { + return in_array($this->type, self::MAY_HAVE_CHILDREN); + } + + public function mayHaveTopics(): bool { + return in_array($this->type, self::MAY_HAVE_TOPICS); + } + + public function hasDescription(): bool { + return $this->desc !== null && $this->desc !== ''; + } + + public function getDescription(): ?string { + return $this->desc; + } + + public function hasIcon(): bool { + return $this->icon !== null && $this->icon !== ''; + } + + public function getIcon(): ?string { + return $this->icon; + } + + public function getIconForDisplay(): string { + if($this->hasIcon()) + return $this->getIcon(); + + if($this->isArchived()) + return 'fas fa-archive fa-fw'; + + return match($this->type) { + self::TYPE_LISTING => 'fas fa-folder fa-fw', + self::TYPE_LINK => 'fas fa-link fa-fw', + default => 'fas fa-comments fa-fw', + }; + } + + public function hasColour(): bool { + return $this->colour !== null && ($this->colour & 0x40000000) === 0; + } + + public function getColourRaw(): ?int { + return $this->colour; + } + + public function getColour(): Colour { + return $this->colour === null ? Colour::none() : Colour::fromMisuzu($this->colour); + } + + public function hasLinkTarget(): bool { + return $this->link !== null && $this->link !== ''; + } + + public function getLinkTarget(): ?string { + return $this->link; + } + + public function hasLinkClicks(): bool { + return $this->clicks !== null; + } + + public function getLinkClicks(): ?int { + return $this->clicks; + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } + + public function isArchived(): bool { + return $this->archived; + } + + public function isHidden(): bool { + return $this->hidden; + } + + public function getTopicsCount(): int { + return $this->topicsCount; + } + + public function getPostsCount(): int { + return $this->postsCount; + } +} diff --git a/src/Forum/ForumPostInfo.php b/src/Forum/ForumPostInfo.php new file mode 100644 index 0000000..416dc90 --- /dev/null +++ b/src/Forum/ForumPostInfo.php @@ -0,0 +1,135 @@ +id = (string)$result->getInteger(0); + $this->topicId = (string)$result->getInteger(1); + $this->categoryId = (string)$result->getInteger(2); + $this->userId = $result->isNull(3) ? null : (string)$result->getInteger(3); + $this->remoteAddr = $result->getString(4); + $this->body = $result->getString(5); + $this->parser = $result->getInteger(6); + $this->displaySignature = $result->getInteger(7) !== 0; + $this->created = $result->getInteger(8); + $this->edited = $result->isNull(9) ? null : $result->getInteger(9); + $this->deleted = $result->isNull(10) ? null : $result->getInteger(10); + } + + public function getId(): string { + return $this->id; + } + + public function getTopicId(): string { + return $this->topicId; + } + + public function getCategoryId(): string { + return $this->categoryId; + } + + public function hasUserId(): bool { + return $this->userId !== null; + } + + public function getUserId(): ?string { + return $this->userId; + } + + public function getRemoteAddressRaw(): string { + return $this->remoteAddr; + } + + public function getRemoteAddress(): IPAddress { + return IPAddress::parse($this->remoteAddr); + } + + public function getBody(): string { + return $this->body; + } + + public function getParser(): int { + return $this->parser; + } + + public function isBodyPlain(): bool { + return $this->parser === Parser::PLAIN; + } + + public function isBodyBBCode(): bool { + return $this->parser === Parser::BBCODE; + } + + public function isBodyMarkdown(): bool { + return $this->parser === Parser::MARKDOWN; + } + + public function shouldDisplaySignature(): bool { + return $this->displaySignature; + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } + + private static ?DateTime $markAsEditedThreshold = null; + public function shouldMarkAsEdited(): bool { + if(self::$markAsEditedThreshold === null) + self::$markAsEditedThreshold = DateTime::now()->modify('-5 minutes'); + + return $this->getCreatedAt()->isMoreThanOrEqual(self::$markAsEditedThreshold); + } + + public function isEdited(): bool { + return $this->edited !== null; + } + + public function getEditedTime(): ?int { + return $this->edited; + } + + public function getEditedAt(): ?DateTime { + return $this->edited === null ? null : DateTime::fromUnixTimeSeconds($this->edited); + } + + private static ?DateTime $canBeDeletedThreshold = null; + public function canBeDeleted(): bool { + if(self::$canBeDeletedThreshold === null) + self::$canBeDeletedThreshold = DateTime::now()->modify('-1 week'); + + return $this->getCreatedAt()->isMoreThanOrEqual(self::$canBeDeletedThreshold); + } + + public function isDeleted(): bool { + return $this->deleted !== null; + } + + public function getDeletedTime(): ?int { + return $this->deleted; + } + + public function getDeletedAt(): ?DateTime { + return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted); + } +} diff --git a/src/Forum/ForumTopicInfo.php b/src/Forum/ForumTopicInfo.php new file mode 100644 index 0000000..2a6c976 --- /dev/null +++ b/src/Forum/ForumTopicInfo.php @@ -0,0 +1,170 @@ + self::TYPE_DISCUSSION, + 'sticky' => self::TYPE_STICKY, + 'announce' => self::TYPE_ANNOUNCE, + 'global' => self::TYPE_GLOBAL, + ]; + + private string $id; + private string $categoryId; + private ?string $userId; + private int $type; + private string $title; + private int $postsCount; + private int $deletedPostsCount; + private int $viewsCount; + private int $created; + private int $bumped; + private ?int $deleted; + private ?int $locked; + + public function __construct(IDbResult $result) { + $this->id = (string)$result->getInteger(0); + $this->categoryId = (string)$result->getInteger(1); + $this->userId = $result->isNull(2) ? null : (string)$result->getInteger(2); + $this->type = $result->getInteger(3); + $this->title = $result->getString(4); + $this->viewsCount = $result->getInteger(5); + $this->created = $result->getInteger(6); + $this->bumped = $result->getInteger(7); + $this->deleted = $result->isNull(8) ? null : $result->getInteger(8); + $this->locked = $result->isNull(9) ? null : $result->getInteger(9); + $this->postsCount = $result->getInteger(10); + $this->deletedPostsCount = $result->getInteger(11); + } + + public function getId(): string { + return $this->id; + } + + public function getCategoryId(): string { + return $this->categoryId; + } + + public function hasUserId(): bool { + return $this->userId !== null; + } + + public function getUserId(): ?string { + return $this->userId; + } + + public function getType(): int { + return $this->type; + } + + public function isDiscussion(): bool { + return $this->type === self::TYPE_DISCUSSION; + } + + public function isSticky(): bool { + return $this->type === self::TYPE_STICKY; + } + + public function isAnnouncement(): bool { + return $this->type === self::TYPE_ANNOUNCE; + } + + public function isGlobalAnnouncement(): bool { + return $this->type === self::TYPE_GLOBAL; + } + + public function isImportant(): bool { + return $this->isSticky() + || $this->isAnnouncement() + || $this->isGlobalAnnouncement(); + } + + public function getIconForDisplay(bool $unread = false): string { + if($this->isDeleted()) + return 'fas fa-trash-alt fa-fw'; + if($this->isAnnouncement() || $this->isGlobalAnnouncement()) + return 'fas fa-bullhorn fa-fw'; + if($this->isSticky()) + return 'fas fa-thumbtack fa-fw'; + if($this->isLocked()) + return 'fas fa-lock fa-fw'; + return sprintf('%s fa-comment fa-fw', $unread ? 'fas' : 'far'); + } + + public function getTitle(): string { + return $this->title; + } + + public function getPostsCount(): int { + return $this->postsCount; + } + + public function getDeletedPostsCount(): int { + return $this->deletedPostsCount; + } + + public function getTotalPostsCount(): int { + return $this->postsCount + $this->deletedPostsCount; + } + + public function getViewsCount(): int { + return $this->viewsCount; + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } + + private static ?DateTime $lastActiveAt = null; + + public function isActive(): bool { + if(self::$lastActiveAt === null) + self::$lastActiveAt = DateTime::now()->modify('-1 month'); + + return $this->getBumpedAt()->isMoreThanOrEqual(self::$lastActiveAt); + } + + public function getBumpedTime(): int { + return $this->bumped; + } + + public function getBumpedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->bumped); + } + + public function isDeleted(): bool { + return $this->deleted !== null; + } + + public function getDeletedTime(): ?int { + return $this->deleted; + } + + public function getDeletedAt(): ?DateTime { + return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted); + } + + public function isLocked(): bool { + return $this->locked !== null; + } + + public function getLockedTime(): ?int { + return $this->locked; + } + + public function getLockedAt(): ?DateTime { + return $this->locked === null ? null : DateTime::fromUnixTimeSeconds($this->locked); + } +} diff --git a/src/Forum/ForumTopicRedirectInfo.php b/src/Forum/ForumTopicRedirectInfo.php new file mode 100644 index 0000000..b59e868 --- /dev/null +++ b/src/Forum/ForumTopicRedirectInfo.php @@ -0,0 +1,43 @@ +topicId = (string)$result->getInteger(0); + $this->userId = $result->isNull(1) ? null : (string)$result->getInteger(1); + $this->link = $result->getString(2); + $this->created = $result->getInteger(3); + } + + public function getTopicId(): string { + return $this->topicId; + } + + public function hasUserId(): bool { + return $this->userId !== null; + } + + public function getUserId(): ?string { + return $this->userId; + } + + public function getLinkTarget(): string { + return $this->link; + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } +} diff --git a/src/Forum/forum.php b/src/Forum/forum.php deleted file mode 100644 index 4c7ed88..0000000 --- a/src/Forum/forum.php +++ /dev/null @@ -1,586 +0,0 @@ - MSZ_FORUM_ROOT, - 'forum_name' => 'Forums', - 'forum_children' => 0, - 'forum_type' => MSZ_FORUM_TYPE_CATEGORY, - 'forum_colour' => null, - 'forum_permissions' => MSZ_FORUM_PERM_SET_READ, -]); - -function forum_is_valid_type(int $type): bool { - return in_array($type, MSZ_FORUM_TYPES, true); -} - -function forum_may_have_children(int $forumType): bool { - return in_array($forumType, MSZ_FORUM_MAY_HAVE_CHILDREN); -} - -function forum_may_have_topics(int $forumType): bool { - return in_array($forumType, MSZ_FORUM_MAY_HAVE_TOPICS); -} - -function forum_get(int $forumId, bool $showDeleted = false): array { - $getForum = \Misuzu\DB::prepare(sprintf( - ' - SELECT - `forum_id`, `forum_name`, `forum_type`, `forum_link`, `forum_archived`, - `forum_link_clicks`, `forum_parent`, `forum_colour`, `forum_icon`, - ( - SELECT COUNT(`topic_id`) - FROM `msz_forum_topics` - WHERE `forum_id` = f.`forum_id` - %1$s - ) as `forum_topic_count` - FROM `msz_forum_categories` as f - WHERE `forum_id` = :forum_id - ', - $showDeleted ? '' : 'AND `topic_deleted` IS NULL' - )); - $getForum->bind('forum_id', $forumId); - return $getForum->fetch(); -} - -function forum_get_root_categories(int $userId): array { - $getCategories = \Misuzu\DB::prepare(sprintf( - ' - SELECT - f.`forum_id`, f.`forum_name`, f.`forum_type`, f.`forum_colour`, f.`forum_icon`, - ( - SELECT COUNT(`forum_id`) - FROM `msz_forum_categories` AS sf - WHERE sf.`forum_parent` = f.`forum_id` - ) AS `forum_children` - FROM `msz_forum_categories` AS f - WHERE f.`forum_parent` = 0 - AND f.`forum_type` = %1$d - AND f.`forum_hidden` = 0 - GROUP BY f.`forum_id` - ORDER BY f.`forum_order` - ', - MSZ_FORUM_TYPE_CATEGORY - )); - $categories = array_merge([MSZ_FORUM_ROOT_DATA], $getCategories->fetchAll()); - - $getRootForumCount = \Misuzu\DB::prepare(sprintf( - " - SELECT COUNT(`forum_id`) - FROM `msz_forum_categories` - WHERE `forum_parent` = %d - AND `forum_type` != %d - ", - MSZ_FORUM_ROOT, - MSZ_FORUM_TYPE_CATEGORY - )); - $categories[0]['forum_children'] = (int)$getRootForumCount->fetchColumn(); - - foreach($categories as $key => $category) { - $categories[$key]['forum_permissions'] = $perms = forum_perms_get_user($category['forum_id'], $userId)[MSZ_FORUM_PERMS_GENERAL]; - - if(!perms_check($perms, MSZ_FORUM_PERM_SET_READ)) { - unset($categories[$key]); - continue; - } - - $categories[$key] = array_merge( - $category, - ['forum_unread' => forum_topics_unread($category['forum_id'], $userId)], - forum_latest_post($category['forum_id'], $userId) - ); - } - - return $categories; -} - -function forum_get_breadcrumbs( - int $forumId, - string $linkFormat = '/forum/forum.php?f=%d', - string $rootFormat = '/forum/#f%d', - array $indexLink = ['Forums' => '/forum/'] -): array { - $breadcrumbs = []; - $getBreadcrumb = \Misuzu\DB::prepare(' - SELECT `forum_id`, `forum_name`, `forum_type`, `forum_parent` - FROM `msz_forum_categories` - WHERE `forum_id` = :forum_id - '); - - while($forumId > 0) { - $getBreadcrumb->bind('forum_id', $forumId); - $breadcrumb = $getBreadcrumb->fetch(); - - if(empty($breadcrumb)) { - break; - } - - $breadcrumbs[$breadcrumb['forum_name']] = sprintf( - $breadcrumb['forum_parent'] === MSZ_FORUM_ROOT - && $breadcrumb['forum_type'] === MSZ_FORUM_TYPE_CATEGORY - ? $rootFormat - : $linkFormat, - $breadcrumb['forum_id'] - ); - $forumId = $breadcrumb['forum_parent']; - } - - return array_reverse($breadcrumbs + $indexLink); -} - -function forum_get_colour(int $forumId): int { - $getColours = \Misuzu\DB::prepare(' - SELECT `forum_id`, `forum_parent`, `forum_colour` - FROM `msz_forum_categories` - WHERE `forum_id` = :forum_id - '); - - while($forumId > 0) { - $getColours->bind('forum_id', $forumId); - $colourInfo = $getColours->fetch(); - - if(empty($colourInfo)) { - break; - } - - if(!empty($colourInfo['forum_colour'])) { - return $colourInfo['forum_colour']; - } - - $forumId = $colourInfo['forum_parent']; - } - - return 0x40000000; -} - -function forum_increment_clicks(int $forumId): void { - $incrementLinkClicks = \Misuzu\DB::prepare(sprintf(' - UPDATE `msz_forum_categories` - SET `forum_link_clicks` = `forum_link_clicks` + 1 - WHERE `forum_id` = :forum_id - AND `forum_type` = %d - AND `forum_link_clicks` IS NOT NULL - ', MSZ_FORUM_TYPE_LINK)); - $incrementLinkClicks->bind('forum_id', $forumId); - $incrementLinkClicks->execute(); -} - -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 0; - - 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_latest_post(int $forumId, int $userId): array { - if($forumId < 1) { - return []; - } - - static $memoized = []; - $memoId = "{$forumId}-{$userId}"; - - if(array_key_exists($memoId, $memoized)) { - return $memoized[$memoId]; - } - - if(!forum_perms_check_user(MSZ_FORUM_PERMS_GENERAL, $forumId, $userId, MSZ_FORUM_PERM_SET_READ)) { - return $memoized[$memoId] = []; - } - - $getLastPost = \Misuzu\DB::prepare(' - SELECT - p.`post_id` AS `recent_post_id`, t.`topic_id` AS `recent_topic_id`, - t.`topic_title` AS `recent_topic_title`, t.`topic_bumped` AS `recent_topic_bumped`, - p.`post_created` AS `recent_post_created`, - u.`user_id` AS `recent_post_user_id`, - u.`username` AS `recent_post_username`, - COALESCE(u.`user_colour`, r.`role_colour`) AS `recent_post_user_colour`, - UNIX_TIMESTAMP(p.`post_created`) AS `post_created_unix` - FROM `msz_forum_posts` AS p - LEFT JOIN `msz_forum_topics` AS t - ON t.`topic_id` = p.`topic_id` - 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 p.`forum_id` = :forum_id - AND p.`post_deleted` IS NULL - ORDER BY p.`post_id` DESC - '); - $getLastPost->bind('forum_id', $forumId); - $currentLast = $getLastPost->fetch(); - - $children = forum_get_child_ids($forumId); - - foreach($children as $child) { - $lastPost = forum_latest_post($child, $userId); - - if(($currentLast['post_created_unix'] ?? 0) < ($lastPost['post_created_unix'] ?? 0)) { - $currentLast = $lastPost; - } - } - - return $memoized[$memoId] = $currentLast; -} - -function forum_get_children(int $parentId, int $userId): array { - $getListing = \Misuzu\DB::prepare(sprintf( - ' - SELECT - :user_id AS `target_user_id`, - f.`forum_id`, f.`forum_name`, f.`forum_description`, f.`forum_type`, f.`forum_icon`, - f.`forum_link`, f.`forum_link_clicks`, f.`forum_archived`, f.`forum_colour`, - f.`forum_count_topics`, f.`forum_count_posts` - FROM `msz_forum_categories` AS f - WHERE f.`forum_parent` = :parent_id - AND f.`forum_hidden` = 0 - AND ( - (f.`forum_parent` = %1$d AND f.`forum_type` != %2$d) - OR f.`forum_parent` != %1$d - ) - GROUP BY f.`forum_id` - ORDER BY f.`forum_order` - ', - MSZ_FORUM_ROOT, - MSZ_FORUM_TYPE_CATEGORY - )); - - $getListing->bind('user_id', $userId); - $getListing->bind('parent_id', $parentId); - - $listing = $getListing->fetchAll(); - - foreach($listing as $key => $forum) { - $listing[$key]['forum_permissions'] = $perms = forum_perms_get_user($forum['forum_id'], $userId)[MSZ_FORUM_PERMS_GENERAL]; - - if(!perms_check($perms, MSZ_FORUM_PERM_SET_READ)) { - unset($listing[$key]); - continue; - } - - $listing[$key] = array_merge( - $forum, - ['forum_unread' => forum_topics_unread($forum['forum_id'], $userId)], - forum_latest_post($forum['forum_id'], $userId) - ); - } - - return $listing; -} - -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 { - // shitty fix for dumb-ass function signature - if($forumId === 0) - $forumId = null; - - 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_posting_info(int $userId): array { - $getPostingInfo = \Misuzu\DB::prepare(' - SELECT - u.`user_id`, u.`username`, u.`user_country`, u.`user_created`, - COALESCE(u.`user_colour`, r.`role_colour`) AS `colour`, - ( - SELECT COUNT(`post_id`) - FROM `msz_forum_posts` - WHERE `user_id` = u.`user_id` - AND `post_deleted` IS NULL - ) AS `user_forum_posts`, - ( - SELECT `post_parse` - FROM `msz_forum_posts` - WHERE `user_id` = u.`user_id` - AND `post_deleted` IS NULL - ORDER BY `post_id` DESC - LIMIT 1 - ) AS `user_post_parse` - FROM `msz_users` as u - LEFT JOIN `msz_roles` AS r - ON r.`role_id` = u.`display_role` - WHERE `user_id` = :user_id - '); - $getPostingInfo->bind('user_id', $userId); - return $getPostingInfo->fetch(); -} - -function forum_count_increase(int $forumId, bool $topic = false): void { - $increaseCount = \Misuzu\DB::prepare(sprintf( - ' - UPDATE `msz_forum_categories` - SET `forum_count_posts` = `forum_count_posts` + 1 - %s - WHERE `forum_id` = :forum - ', - $topic ? ',`forum_count_topics` = `forum_count_topics` + 1' : '' - )); - $increaseCount->bind('forum', $forumId); - $increaseCount->execute(); -} - -function forum_count_synchronise(int $forumId = MSZ_FORUM_ROOT, bool $save = true): array { - static $getChildren = null; - static $getCounts = null; - static $setCounts = null; - - if(is_null($getChildren)) { - $getChildren = \Misuzu\DB::prepare(' - SELECT `forum_id`, `forum_parent` - FROM `msz_forum_categories` - WHERE `forum_parent` = :parent - '); - } - - if(is_null($getCounts)) { - $getCounts = \Misuzu\DB::prepare(' - SELECT :forum as `target_forum_id`, - ( - SELECT COUNT(`topic_id`) - FROM `msz_forum_topics` - WHERE `forum_id` = `target_forum_id` - AND `topic_deleted` IS NULL - ) AS `count_topics`, - ( - SELECT COUNT(`post_id`) - FROM `msz_forum_posts` - WHERE `forum_id` = `target_forum_id` - AND `post_deleted` IS NULL - ) AS `count_posts` - '); - } - - if($save && is_null($setCounts)) { - $setCounts = \Misuzu\DB::prepare(' - UPDATE `msz_forum_categories` - SET `forum_count_topics` = :topics, - `forum_count_posts` = :posts - WHERE `forum_id` = :forum_id - '); - } - - $getChildren->bind('parent', $forumId); - $children = $getChildren->fetchAll(); - - $topics = 0; - $posts = 0; - - foreach($children as $child) { - $childCount = forum_count_synchronise($child['forum_id'], $save); - $topics += $childCount['topics']; - $posts += $childCount['posts']; - } - - $getCounts->bind('forum', $forumId); - $counts = $getCounts->fetch(); - $topics += $counts['count_topics']; - $posts += $counts['count_posts']; - - if($forumId > 0 && $save) { - $setCounts->bind('forum_id', $forumId); - $setCounts->bind('topics', $topics); - $setCounts->bind('posts', $posts); - $setCounts->execute(); - } - - return compact('topics', 'posts'); -} - -function forum_get_user_most_active_category_info(string|int $userId): ?object { - if(is_string($userId)) - $userId = (int)$userId; - if($userId < 1) - return null; - - global $cfg; - - $getActiveForum = \Misuzu\DB::prepare(sprintf( - 'SELECT forum_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = :user AND post_deleted IS NULL AND forum_id NOT IN (%s) GROUP BY forum_id ORDER BY post_count DESC LIMIT 1', - implode(',', $cfg->getArray('forum_leader.unranked.forum')) - )); - $getActiveForum->bind('user', $userId); - - return $getActiveForum->fetchObject(); -} - -function forum_get_user_topic_count(\Misuzu\Users\UserInfo|string|int $userId): int { - if(is_int($userId)) - $userId = (string)$userId; - elseif($userId instanceof \Misuzu\Users\UserInfo) - $userId = $userId->getId(); - - global $db; - static $stmt = null; - if($stmt === null) - $stmt = $db->prepare('SELECT COUNT(*) FROM msz_forum_topics WHERE user_id = ? AND topic_deleted IS NULL'); - else - $stmt->reset(); - $stmt->addParameter(1, $userId); - $stmt->execute(); - - $result = $stmt->getResult(); - return $result->next() ? $result->getInteger(0) : 0; -} - -function forum_get_user_post_count(\Misuzu\Users\UserInfo|string|int $userId): int { - if(is_int($userId)) - $userId = (string)$userId; - elseif($userId instanceof \Misuzu\Users\UserInfo) - $userId = $userId->getId(); - - global $db; - static $stmt = null; - if($stmt === null) - $stmt = $db->prepare('SELECT COUNT(*) FROM msz_forum_posts WHERE user_id = ? AND post_deleted IS NULL'); - else - $stmt->reset(); - $stmt->addParameter(1, $userId); - $stmt->execute(); - - $result = $stmt->getResult(); - return $result->next() ? $result->getInteger(0) : 0; -} diff --git a/src/Forum/leaderboard.php b/src/Forum/leaderboard.php deleted file mode 100644 index 7d528f5..0000000 --- a/src/Forum/leaderboard.php +++ /dev/null @@ -1,98 +0,0 @@ -= MSZ_FORUM_LEADERBOARD_START_YEAR && $year <= date('Y'); -} - -function forum_leaderboard_month_valid(?int $year, ?int $month): bool { - if(is_null($month) || !forum_leaderboard_year_valid($year) || $month < 1 || $month > 12) { - return false; - } - - $combo = sprintf('%04d%02d', $year, $month); - $start = sprintf('%04d%02d', MSZ_FORUM_LEADERBOARD_START_YEAR, MSZ_FORUM_LEADERBOARD_START_MONTH); - $current = date('Ym'); - - return $combo >= $start && $combo <= $current; -} - -function forum_leaderboard_categories(): array { - $categories = [ - MSZ_FORUM_LEADERBOARD_CATEGORY_ALL => 'All Time', - ]; - - $currentYear = date('Y'); - $currentMonth = date('m'); - - for($i = $currentYear; $i >= MSZ_FORUM_LEADERBOARD_START_YEAR; $i--) { - $categories[$i] = sprintf('Leaderboard %d', $i); - } - - for($i = $currentYear, $j = $currentMonth;;) { - $categories[sprintf('%d%02d', $i, $j)] = sprintf('Leaderboard %d-%02d', $i, $j); - - if($j <= 1) { - $i--; $j = 12; - } else $j--; - - if($i <= MSZ_FORUM_LEADERBOARD_START_YEAR && $j < MSZ_FORUM_LEADERBOARD_START_MONTH) - break; - } - - return $categories; -} - -function forum_leaderboard_listing( - ?int $year = null, - ?int $month = null, - array $unrankedForums = [], - array $unrankedTopics = [] -): array { - $hasYear = forum_leaderboard_year_valid($year); - $hasMonth = $hasYear && forum_leaderboard_month_valid($year, $month); - $unrankedForums = implode(',', $unrankedForums); - $unrankedTopics = implode(',', $unrankedTopics); - - $rawLeaderboard = \Misuzu\DB::query(sprintf( - ' - SELECT - u.`user_id`, u.`username`, - COUNT(fp.`post_id`) as `posts` - FROM `msz_users` AS u - INNER JOIN `msz_forum_posts` AS fp - ON fp.`user_id` = u.`user_id` - WHERE fp.`post_deleted` IS NULL - %s %s %s - GROUP BY u.`user_id` - HAVING `posts` > 0 - ORDER BY `posts` DESC - ', - $unrankedForums ? sprintf('AND fp.`forum_id` NOT IN (%s)', $unrankedForums) : '', - $unrankedTopics ? sprintf('AND fp.`topic_id` NOT IN (%s)', $unrankedTopics) : '', - !$hasYear ? '' : sprintf( - 'AND DATE(fp.`post_created`) BETWEEN \'%1$04d-%2$02d-01\' AND \'%1$04d-%3$02d-31\'', - $year, - $hasMonth ? $month : 1, - $hasMonth ? $month : 12 - ) - ))->fetchAll(); - - $leaderboard = []; - $ranking = 0; - $lastPosts = null; - - foreach($rawLeaderboard as $entry) { - if(is_null($lastPosts) || $lastPosts > $entry['posts']) { - $ranking++; - $lastPosts = $entry['posts']; - } - - $entry['rank'] = $ranking; - $leaderboard[] = $entry; - } - - return $leaderboard; -} diff --git a/src/Forum/perms.php b/src/Forum/perms.php deleted file mode 100644 index a3d79a1..0000000 --- a/src/Forum/perms.php +++ /dev/null @@ -1,212 +0,0 @@ - 0) { - $perms = forum_perms_get_user( - forum_get_parent_id($forum), - $user - ); - } - - $getPerms = \Misuzu\DB::prepare(sprintf( - ' - SELECT %s - FROM `msz_forum_permissions` - WHERE (`forum_id` = :forum_id OR `forum_id` IS NULL) - AND ( - (`user_id` IS NULL AND `role_id` IS NULL) - OR (`user_id` = :user_id_1 AND `role_id` IS NULL) - OR ( - `user_id` IS NULL - AND `role_id` IN ( - SELECT `role_id` - FROM `msz_users_roles` - WHERE `user_id` = :user_id_2 - ) - ) - ) - ', - perms_get_select(MSZ_FORUM_PERM_MODES) - )); - $getPerms->bind('forum_id', $forum); - $getPerms->bind('user_id_1', $user); - $getPerms->bind('user_id_2', $user); - - $userPerms = $getPerms->fetch(); - foreach($perms as $key => $value) - $perms[$key] |= $userPerms[$key] ?? 0; - - return $memo[$memoId] = $perms; -} - -function forum_perms_get_role(?int $forum, int $role): array { - $perms = perms_get_blank(MSZ_FORUM_PERM_MODES); - - if($role < 1 || $forum < 0) { - return $perms; - } - - static $memo = []; - $memoId = "{$forum}-{$role}"; - - if(array_key_exists($memoId, $memo)) { - return $memo[$memoId]; - } - - if($forum > 0) { - $perms = forum_perms_get_role( - forum_get_parent_id($forum), - $role - ); - } - - $getPerms = \Misuzu\DB::prepare(sprintf( - ' - SELECT %s - FROM `msz_forum_permissions` - WHERE (`forum_id` = :forum_id OR `forum_id` IS NULL) - AND `role_id` = :role_id - AND `user_id` IS NULL - ', - perms_get_select(MSZ_FORUM_PERM_MODES) - )); - $getPerms->bind('forum_id', $forum); - $getPerms->bind('role_id', $role); - - $userPerms = $getPerms->fetch(); - foreach($perms as $key => $value) - $perms[$key] |= $userPerms[$key] ?? 0; - - return $memo[$memoId] = $perms; -} - -function forum_perms_get_user_raw(?int $forum, int $user): array { - if($user < 1) { - return perms_create(MSZ_FORUM_PERM_MODES); - } - - $getPerms = \Misuzu\DB::prepare(sprintf( - ' - SELECT `%s` - FROM `msz_forum_permissions` - WHERE `forum_id` %s - AND `user_id` = :user_id - AND `role_id` IS NULL - ', - implode('`, `', perms_get_keys(MSZ_FORUM_PERM_MODES)), - $forum === null ? 'IS NULL' : '= :forum_id' - )); - - if($forum !== null) { - $getPerms->bind('forum_id', $forum); - } - - $getPerms->bind('user_id', $user); - $perms = $getPerms->fetch(); - - if(empty($perms)) { - return perms_create(MSZ_FORUM_PERM_MODES); - } - - return $perms; -} - -function forum_perms_get_role_raw(?int $forum, ?int $role): array { - if($role < 1 && $role !== null) { - return perms_create(MSZ_FORUM_PERM_MODES); - } - - $getPerms = \Misuzu\DB::prepare(sprintf( - ' - SELECT `%s` - FROM `msz_forum_permissions` - WHERE `forum_id` %s - AND `user_id` IS NULL - AND `role_id` %s - ', - implode('`, `', perms_get_keys(MSZ_FORUM_PERM_MODES)), - $forum === null ? 'IS NULL' : '= :forum_id', - $role === null ? 'IS NULL' : '= :role_id' - )); - - if($forum !== null) { - $getPerms->bind('forum_id', $forum); - } - - if($role !== null) { - $getPerms->bind('role_id', $role); - } - - $perms = $getPerms->fetch(); - - if(empty($perms)) { - return perms_create(MSZ_FORUM_PERM_MODES); - } - - return $perms; -} - -function forum_perms_check_user( - string $prefix, - ?int $forumId, - ?int $userId, - int $perm, - bool $strict = false -): bool { - return perms_check(forum_perms_get_user($forumId, $userId)[$prefix] ?? 0, $perm, $strict); -} diff --git a/src/Forum/post.php b/src/Forum/post.php deleted file mode 100644 index fdf10cc..0000000 --- a/src/Forum/post.php +++ /dev/null @@ -1,363 +0,0 @@ -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 { - // i have no idea if the post_created field depend on not being parsed, so post_created_unix it is! - // not even the first time i've done this either (see forum_latest_post) lol, what a mess - $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`, - UNIX_TIMESTAMP(p.`post_created`) AS `post_created_unix`, - 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 727b8ba..0000000 --- a/src/Forum/topic.php +++ /dev/null @@ -1,719 +0,0 @@ -bind('forum_id', $forumId); - $createTopic->bind('user_id', $userId); - $createTopic->bind('topic_title', $title); - $createTopic->bind('topic_type', $type); - - return $createTopic->execute() ? \Misuzu\DB::lastId() : 0; -} - -function forum_topic_update(int $topicId, ?string $title, ?int $type = null): bool { - if($topicId < 1) { - return false; - } - - // make sure it's null and not some other kinda empty - if(empty($title)) { - $title = null; - } - - if($type !== null && !forum_topic_is_valid_type($type)) { - return false; - } - - $updateTopic = \Misuzu\DB::prepare(' - UPDATE `msz_forum_topics` - SET `topic_title` = COALESCE(:topic_title, `topic_title`), - `topic_type` = COALESCE(:topic_type, `topic_type`) - WHERE `topic_id` = :topic_id - '); - $updateTopic->bind('topic_id', $topicId); - $updateTopic->bind('topic_title', $title); - $updateTopic->bind('topic_type', $type); - - return $updateTopic->execute(); -} - -function forum_topic_get(int $topicId, bool $allowDeleted = false): array { - $getTopic = \Misuzu\DB::prepare(sprintf( - ' - SELECT - t.`topic_id`, t.`forum_id`, t.`topic_title`, t.`topic_type`, t.`topic_locked`, t.`topic_created`, - f.`forum_archived` AS `topic_archived`, t.`topic_deleted`, t.`topic_bumped`, f.`forum_type`, - fp.`topic_id` AS `author_post_id`, fp.`user_id` AS `author_user_id`, - ( - SELECT COUNT(`post_id`) - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - AND `post_deleted` IS NULL - ) AS `topic_count_posts`, - ( - SELECT COUNT(`post_id`) - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - AND `post_deleted` IS NOT NULL - ) AS `topic_count_posts_deleted` - FROM `msz_forum_topics` AS t - LEFT JOIN `msz_forum_categories` AS f - ON f.`forum_id` = t.`forum_id` - LEFT JOIN `msz_forum_posts` AS fp - ON fp.`post_id` = ( - SELECT MIN(`post_id`) - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - ) - WHERE t.`topic_id` = :topic_id - %s - ', - $allowDeleted ? '' : 'AND t.`topic_deleted` IS NULL' - )); - $getTopic->bind('topic_id', $topicId); - return $getTopic->fetch(); -} - -function forum_topic_redir_info(int $topicId): ?object { - $getTopicRedir = \Misuzu\DB::prepare(' - SELECT topic_id, user_id, topic_redir_url, - UNIX_TIMESTAMP(topic_redir_created) AS topic_redir_created - FROM msz_forum_topics_redirects - WHERE topic_id = :topic_id - '); - $getTopicRedir->bind('topic_id', $topicId); - return $getTopicRedir->fetchObject(); -} - -function forum_topic_redir_count(): int { - return \Misuzu\DB::query('SELECT COUNT(*) FROM msz_forum_topics_redirects')->fetchColumn() ?? 0; -} - -function forum_topic_redir_all(int $offset, int $take): array { - $getTopicRedirs = \Misuzu\DB::prepare(' - SELECT topic_id, user_id, topic_redir_url, - UNIX_TIMESTAMP(topic_redir_created) AS topic_redir_created - FROM msz_forum_topics_redirects - LIMIT :offset, :take - '); - $getTopicRedirs->bind('offset', $offset); - $getTopicRedirs->bind('take', $take); - return $getTopicRedirs->fetchObjects(); -} - -function forum_topic_redir_create(int $topicId, int $userId, string $url): void { - if($topicId < 1 || empty($url)) return; - if($userId < 1) $userId = null; - - $createTopicRedir = \Misuzu\DB::prepare(' - INSERT INTO msz_forum_topics_redirects (topic_id, user_id, topic_redir_url) - VALUES (:topic_id, :user_id, :redir_url) - '); - $createTopicRedir->bind('topic_id', $topicId); - $createTopicRedir->bind('user_id', $userId); - $createTopicRedir->bind('redir_url', $url); - $createTopicRedir->execute(); -} - -function forum_topic_redir_remove(int $topicId): void { - $removeTopicRedir = \Misuzu\DB::prepare(' - DELETE FROM msz_forum_topics_redirects - WHERE topic_id = :topic_id - '); - $removeTopicRedir->bind('topic_id', $topicId); - $removeTopicRedir->execute(); -} - -function forum_topic_bump(int $topicId): bool { - $bumpTopic = \Misuzu\DB::prepare(' - UPDATE `msz_forum_topics` - SET `topic_bumped` = NOW() - WHERE `topic_id` = :topic_id - AND `topic_deleted` IS NULL - '); - $bumpTopic->bind('topic_id', $topicId); - return $bumpTopic->execute(); -} - -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 - 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(); - } -} - -function forum_topic_listing( - int $forumId, int $userId, - int $offset = 0, int $take = 0, - bool $showDeleted = false, bool $sortByPriority = false -): array { - $hasPagination = $offset >= 0 && $take > 0; - $getTopics = \Misuzu\DB::prepare(sprintf( - ' - SELECT - :user_id AS `target_user_id`, - t.`topic_id`, t.`topic_title`, t.`topic_locked`, t.`topic_type`, t.`topic_created`, - t.`topic_bumped`, t.`topic_deleted`, t.`topic_count_views`, f.`forum_type`, - au.`user_id` AS `author_id`, au.`username` AS `author_name`, - COALESCE(au.`user_colour`, ar.`role_colour`) AS `author_colour`, - lp.`post_id` AS `response_id`, - lp.`post_created` AS `response_created`, - lu.`user_id` AS `respondent_id`, - lu.`username` AS `respondent_name`, - COALESCE(lu.`user_colour`, lr.`role_colour`) AS `respondent_colour`, - ( - SELECT COUNT(`post_id`) - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - %5$s - ) AS `topic_count_posts`, - ( - SELECT CEIL(COUNT(`post_id`) / %6$d) - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - %5$s - ) AS `topic_pages`, - ( - SELECT - `target_user_id` > 0 - AND - t.`topic_bumped` > NOW() - INTERVAL 1 MONTH - AND ( - SELECT COUNT(ti.`topic_id`) < 1 - FROM `msz_forum_topics_track` AS tt - RIGHT JOIN `msz_forum_topics` AS ti - ON ti.`topic_id` = tt.`topic_id` - WHERE ti.`topic_id` = t.`topic_id` - AND tt.`user_id` = `target_user_id` - AND `track_last_read` >= `topic_bumped` - ) - ) AS `topic_unread`, - ( - SELECT COUNT(`post_id`) > 0 - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - AND `user_id` = `target_user_id` - LIMIT 1 - ) AS `topic_participated` - FROM `msz_forum_topics` AS t - LEFT JOIN `msz_forum_categories` AS f - ON f.`forum_id` = t.`forum_id` - LEFT JOIN `msz_users` AS au - ON t.`user_id` = au.`user_id` - LEFT JOIN `msz_roles` AS ar - ON ar.`role_id` = au.`display_role` - LEFT JOIN `msz_forum_posts` AS lp - ON lp.`post_id` = ( - SELECT `post_id` - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - %5$s - ORDER BY `post_id` DESC - LIMIT 1 - ) - LEFT JOIN `msz_users` AS lu - ON lu.`user_id` = lp.`user_id` - LEFT JOIN `msz_roles` AS lr - ON lr.`role_id` = lu.`display_role` - WHERE ( - t.`forum_id` = :forum_id - OR t.`topic_type` = %3$d - ) - %1$s - GROUP BY t.`topic_id` - ORDER BY FIELD(t.`topic_type`, %4$s) DESC, t.`topic_bumped` DESC - %2$s - ', - $showDeleted ? '' : 'AND t.`topic_deleted` IS NULL', - $hasPagination ? 'LIMIT :offset, :take' : '', - MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT, - implode(',', array_reverse(MSZ_TOPIC_TYPE_ORDER)), - $showDeleted ? '' : 'AND `post_deleted` IS NULL', - MSZ_FORUM_POSTS_PER_PAGE - )); - $getTopics->bind('forum_id', $forumId); - $getTopics->bind('user_id', $userId); - - if($hasPagination) { - $getTopics->bind('offset', $offset); - $getTopics->bind('take', $take); - } - - return $getTopics->fetchAll(); -} - -function forum_topic_count_user(int $authorId, int $userId, bool $showDeleted = false): int { - $getTopics = \Misuzu\DB::prepare(sprintf( - ' - SELECT COUNT(`topic_id`) - FROM `msz_forum_topics` AS t - WHERE t.`user_id` = :author_id - %1$s - ', - $showDeleted ? '' : 'AND t.`topic_deleted` IS NULL' - )); - $getTopics->bind('author_id', $authorId); - //$getTopics->bind('user_id', $userId); - - return (int)$getTopics->fetchColumn(); -} - -// Remove unneccesary stuff from the sql stmt -function forum_topic_listing_user( - int $authorId, - int $userId, - int $offset = 0, - int $take = 0, - bool $showDeleted = false -): array { - $hasPagination = $offset >= 0 && $take > 0; - $getTopics = \Misuzu\DB::prepare(sprintf( - ' - SELECT - :user_id AS `target_user_id`, - t.`topic_id`, t.`topic_title`, t.`topic_locked`, t.`topic_type`, t.`topic_created`, - t.`topic_bumped`, t.`topic_deleted`, t.`topic_count_views`, f.`forum_type`, - au.`user_id` AS `author_id`, au.`username` AS `author_name`, - COALESCE(au.`user_colour`, ar.`role_colour`) AS `author_colour`, - lp.`post_id` AS `response_id`, - lp.`post_created` AS `response_created`, - lu.`user_id` AS `respondent_id`, - lu.`username` AS `respondent_name`, - COALESCE(lu.`user_colour`, lr.`role_colour`) AS `respondent_colour`, - ( - SELECT COUNT(`post_id`) - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - %5$s - ) AS `topic_count_posts`, - ( - SELECT CEIL(COUNT(`post_id`) / %6$d) - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - %5$s - ) AS `topic_pages`, - ( - SELECT - `target_user_id` > 0 - AND - t.`topic_bumped` > NOW() - INTERVAL 1 MONTH - AND ( - SELECT COUNT(ti.`topic_id`) < 1 - FROM `msz_forum_topics_track` AS tt - RIGHT JOIN `msz_forum_topics` AS ti - ON ti.`topic_id` = tt.`topic_id` - WHERE ti.`topic_id` = t.`topic_id` - AND tt.`user_id` = `target_user_id` - AND `track_last_read` >= `topic_bumped` - ) - ) AS `topic_unread`, - ( - SELECT COUNT(`post_id`) > 0 - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - AND `user_id` = `target_user_id` - LIMIT 1 - ) AS `topic_participated` - FROM `msz_forum_topics` AS t - LEFT JOIN `msz_forum_categories` AS f - ON f.`forum_id` = t.`forum_id` - LEFT JOIN `msz_users` AS au - ON t.`user_id` = au.`user_id` - LEFT JOIN `msz_roles` AS ar - ON ar.`role_id` = au.`display_role` - LEFT JOIN `msz_forum_posts` AS lp - ON lp.`post_id` = ( - SELECT `post_id` - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - %5$s - ORDER BY `post_id` DESC - LIMIT 1 - ) - LEFT JOIN `msz_users` AS lu - ON lu.`user_id` = lp.`user_id` - LEFT JOIN `msz_roles` AS lr - ON lr.`role_id` = lu.`display_role` - WHERE au.`user_id` = :author_id - %1$s - ORDER BY FIELD(t.`topic_type`, %4$s) DESC, t.`topic_bumped` DESC - %2$s - ', - $showDeleted ? '' : 'AND t.`topic_deleted` IS NULL', - $hasPagination ? 'LIMIT :offset, :take' : '', - MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT, - implode(',', array_reverse(MSZ_TOPIC_TYPE_ORDER)), - $showDeleted ? '' : 'AND `post_deleted` IS NULL', - MSZ_FORUM_POSTS_PER_PAGE - )); - $getTopics->bind('author_id', $authorId); - $getTopics->bind('user_id', $userId); - - if($hasPagination) { - $getTopics->bind('offset', $offset); - $getTopics->bind('take', $take); - } - - return $getTopics->fetchAll(); -} - -function forum_topic_listing_search(string $query, int $userId): array { - $getTopics = \Misuzu\DB::prepare(sprintf( - ' - SELECT - :user_id AS `target_user_id`, - t.`topic_id`, t.`topic_title`, t.`topic_locked`, t.`topic_type`, t.`topic_created`, - t.`topic_bumped`, t.`topic_deleted`, t.`topic_count_views`, f.`forum_type`, - au.`user_id` AS `author_id`, au.`username` AS `author_name`, - COALESCE(au.`user_colour`, ar.`role_colour`) AS `author_colour`, - lp.`post_id` AS `response_id`, - lp.`post_created` AS `response_created`, - lu.`user_id` AS `respondent_id`, - lu.`username` AS `respondent_name`, - COALESCE(lu.`user_colour`, lr.`role_colour`) AS `respondent_colour`, - ( - SELECT COUNT(`post_id`) - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - AND `post_deleted` IS NULL - ) AS `topic_count_posts`, - ( - SELECT CEIL(COUNT(`post_id`) / %2$d) - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - AND `post_deleted` IS NULL - ) AS `topic_pages`, - ( - SELECT - `target_user_id` > 0 - AND - t.`topic_bumped` > NOW() - INTERVAL 1 MONTH - AND ( - SELECT COUNT(ti.`topic_id`) < 1 - FROM `msz_forum_topics_track` AS tt - RIGHT JOIN `msz_forum_topics` AS ti - ON ti.`topic_id` = tt.`topic_id` - WHERE ti.`topic_id` = t.`topic_id` - AND tt.`user_id` = `target_user_id` - AND `track_last_read` >= `topic_bumped` - ) - ) AS `topic_unread`, - ( - SELECT COUNT(`post_id`) > 0 - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - AND `user_id` = `target_user_id` - LIMIT 1 - ) AS `topic_participated` - FROM `msz_forum_topics` AS t - LEFT JOIN `msz_forum_categories` AS f - ON f.`forum_id` = t.`forum_id` - LEFT JOIN `msz_users` AS au - ON t.`user_id` = au.`user_id` - LEFT JOIN `msz_roles` AS ar - ON ar.`role_id` = au.`display_role` - LEFT JOIN `msz_forum_posts` AS lp - ON lp.`post_id` = ( - SELECT `post_id` - FROM `msz_forum_posts` - WHERE `topic_id` = t.`topic_id` - AND `post_deleted` IS NULL - ORDER BY `post_id` DESC - LIMIT 1 - ) - LEFT JOIN `msz_users` AS lu - ON lu.`user_id` = lp.`user_id` - LEFT JOIN `msz_roles` AS lr - ON lr.`role_id` = lu.`display_role` - WHERE MATCH(`topic_title`) - AGAINST (:query IN NATURAL LANGUAGE MODE) - AND t.`topic_deleted` IS NULL - ORDER BY FIELD(t.`topic_type`, %1$s) DESC, t.`topic_bumped` DESC - ', - implode(',', array_reverse(MSZ_TOPIC_TYPE_ORDER)), - MSZ_FORUM_POSTS_PER_PAGE - )); - $getTopics->bind('query', $query); - $getTopics->bind('user_id', $userId); - - return $getTopics->fetchAll(); -} - -function forum_topic_lock(int $topicId): bool { - if($topicId < 1) { - return false; - } - - $markLocked = \Misuzu\DB::prepare(' - UPDATE `msz_forum_topics` - SET `topic_locked` = NOW() - WHERE `topic_id` = :topic - AND `topic_locked` IS NULL - '); - $markLocked->bind('topic', $topicId); - - return $markLocked->execute(); -} - -function forum_topic_unlock(int $topicId): bool { - if($topicId < 1) { - return false; - } - - $markUnlocked = \Misuzu\DB::prepare(' - UPDATE `msz_forum_topics` - SET `topic_locked` = NULL - WHERE `topic_id` = :topic - AND `topic_locked` IS NOT NULL - '); - $markUnlocked->bind('topic', $topicId); - - return $markUnlocked->execute(); -} - -define('MSZ_E_FORUM_TOPIC_DELETE_OK', 0); // deleting is fine -define('MSZ_E_FORUM_TOPIC_DELETE_USER', 1); // invalid user -define('MSZ_E_FORUM_TOPIC_DELETE_TOPIC', 2); // topic doesn't exist -define('MSZ_E_FORUM_TOPIC_DELETE_DELETED', 3); // topic is already marked as deleted -define('MSZ_E_FORUM_TOPIC_DELETE_OWNER', 4); // you may only delete your own topics -define('MSZ_E_FORUM_TOPIC_DELETE_OLD', 5); // topic has existed for too long to be deleted -define('MSZ_E_FORUM_TOPIC_DELETE_PERM', 6); // you aren't allowed to delete topics -define('MSZ_E_FORUM_TOPIC_DELETE_POSTS', 7); // the topic already has replies - -// only allow topics made within a day of posting to be deleted by normal users -define('MSZ_FORUM_TOPIC_DELETE_TIME_LIMIT', 60 * 60 * 24); - -// only allow topics with a single post to be deleted, includes soft deleted posts -define('MSZ_FORUM_TOPIC_DELETE_POST_LIMIT', 1); - -// set $userId to null for system request, make sure this is NEVER EVER null on user request -// $topicId can also be a the return value of forum_topic_get if you already grabbed it once before -function forum_topic_can_delete($topicId, ?int $userId = null): int { - if($userId !== null && $userId < 1) { - return MSZ_E_FORUM_TOPIC_DELETE_USER; - } - - if(is_array($topicId)) { - $topic = $topicId; - } else { - $topic = forum_topic_get((int)$topicId, true); - } - - if(empty($topic)) { - return MSZ_E_FORUM_TOPIC_DELETE_TOPIC; - } - - $isSystemReq = $userId === null; - $perms = $isSystemReq ? 0 : forum_perms_get_user($topic['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($topic['topic_deleted']); - - if(!$canViewPost) { - return MSZ_E_FORUM_TOPIC_DELETE_TOPIC; - } - - if($postIsDeleted) { - return $canDeleteAny ? MSZ_E_FORUM_TOPIC_DELETE_DELETED : MSZ_E_FORUM_TOPIC_DELETE_TOPIC; - } - - if($isSystemReq) { - return MSZ_E_FORUM_TOPIC_DELETE_OK; - } - - if(!$canDeleteAny) { - if(!perms_check($perms, MSZ_FORUM_PERM_DELETE_POST)) { - return MSZ_E_FORUM_TOPIC_DELETE_PERM; - } - - if($topic['author_user_id'] !== $userId) { - return MSZ_E_FORUM_TOPIC_DELETE_OWNER; - } - - if(strtotime($topic['topic_created']) <= (time() - MSZ_FORUM_TOPIC_DELETE_TIME_LIMIT)) { - return MSZ_E_FORUM_TOPIC_DELETE_OLD; - } - - $totalReplies = (int)$topic['topic_count_posts'] + (int)$topic['topic_count_posts_deleted']; - - if($totalReplies > MSZ_E_FORUM_TOPIC_DELETE_POSTS) { - return MSZ_E_FORUM_TOPIC_DELETE_POSTS; - } - } - - return MSZ_E_FORUM_TOPIC_DELETE_OK; -} - -function forum_topic_delete(int $topicId): bool { - if($topicId < 1) { - return false; - } - - $markTopicDeleted = \Misuzu\DB::prepare(' - UPDATE `msz_forum_topics` - SET `topic_deleted` = NOW() - WHERE `topic_id` = :topic - AND `topic_deleted` IS NULL - '); - $markTopicDeleted->bind('topic', $topicId); - - if(!$markTopicDeleted->execute()) { - return false; - } - - $markPostsDeleted = \Misuzu\DB::prepare(' - UPDATE `msz_forum_posts` as p - SET p.`post_deleted` = ( - SELECT `topic_deleted` - FROM `msz_forum_topics` - WHERE `topic_id` = p.`topic_id` - ) - WHERE p.`topic_id` = :topic - AND p.`post_deleted` IS NULL - '); - $markPostsDeleted->bind('topic', $topicId); - - return $markPostsDeleted->execute(); -} - -function forum_topic_restore(int $topicId): bool { - if($topicId < 1) { - return false; - } - - $markPostsRestored = \Misuzu\DB::prepare(' - UPDATE `msz_forum_posts` as p - SET p.`post_deleted` = NULL - WHERE p.`topic_id` = :topic - AND p.`post_deleted` = ( - SELECT `topic_deleted` - FROM `msz_forum_topics` - WHERE `topic_id` = p.`topic_id` - ) - '); - $markPostsRestored->bind('topic', $topicId); - - if(!$markPostsRestored->execute()) { - return false; - } - - $markTopicRestored = \Misuzu\DB::prepare(' - UPDATE `msz_forum_topics` - SET `topic_deleted` = NULL - WHERE `topic_id` = :topic - AND `topic_deleted` IS NOT NULL - '); - $markTopicRestored->bind('topic', $topicId); - - return $markTopicRestored->execute(); -} - -function forum_topic_nuke(int $topicId): bool { - if($topicId < 1) { - return false; - } - - $nukeTopic = \Misuzu\DB::prepare(' - DELETE FROM `msz_forum_topics` - WHERE `topic_id` = :topic - '); - $nukeTopic->bind('topic', $topicId); - return $nukeTopic->execute(); -} - -function forum_get_user_most_active_topic_info(int $userId): ?object { - if($userId < 1) - return null; - - global $cfg; - - $getActiveForum = \Misuzu\DB::prepare(sprintf( - 'SELECT topic_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = :user AND post_deleted IS NULL AND forum_id NOT IN (%s) GROUP BY topic_id ORDER BY post_count DESC LIMIT 1', - implode(',', $cfg->getArray('forum_leader.unranked.forum')) - )); - $getActiveForum->bind('user', $userId); - - return $getActiveForum->fetchObject(); -} diff --git a/src/Forum/validate.php b/src/Forum/validate.php deleted file mode 100644 index 5c0a6ba..0000000 --- a/src/Forum/validate.php +++ /dev/null @@ -1,33 +0,0 @@ - MSZ_TOPIC_TITLE_LENGTH_MAX) { - return 'too-long'; - } - - return ''; -} - -function forum_validate_post(string $text): string { - $length = mb_strlen(trim($text)); - - if($length < MSZ_POST_TEXT_LENGTH_MIN) { - return 'too-short'; - } - - if($length > MSZ_POST_TEXT_LENGTH_MAX) { - return 'too-long'; - } - - return ''; -} diff --git a/src/MisuzuContext.php b/src/MisuzuContext.php index c2f7727..9e25484 100644 --- a/src/MisuzuContext.php +++ b/src/MisuzuContext.php @@ -22,6 +22,7 @@ use Misuzu\Comments\Comments; use Misuzu\Config\IConfig; use Misuzu\Counters\Counters; use Misuzu\Emoticons\Emotes; +use Misuzu\Forum\Forum; use Misuzu\Home\HomeRoutes; use Misuzu\Info\InfoRoutes; use Misuzu\News\News; @@ -64,28 +65,30 @@ class MisuzuContext { private Sessions $sessions; private Counters $counters; private ProfileFields $profileFields; + private Forum $forum; private AuthInfo $authInfo; public function __construct(IDbConnection $dbConn, IConfig $config) { $this->dbConn = $dbConn; $this->config = $config; $this->auditLog = new AuditLog($this->dbConn); - $this->emotes = new Emotes($this->dbConn); - $this->changelog = new Changelog($this->dbConn); - $this->news = new News($this->dbConn); - $this->comments = new Comments($this->dbConn); - $this->loginAttempts = new LoginAttempts($this->dbConn); - $this->recoveryTokens = new RecoveryTokens($this->dbConn); - $this->modNotes = new ModNotes($this->dbConn); - $this->bans = new Bans($this->dbConn); - $this->warnings = new Warnings($this->dbConn); - $this->tfaSessions = new TwoFactorAuthSessions($this->dbConn); - $this->roles = new Roles($this->dbConn); - $this->users = new Users($this->dbConn); - $this->sessions = new Sessions($this->dbConn); - $this->counters = new Counters($this->dbConn); - $this->profileFields = new ProfileFields($this->dbConn); $this->authInfo = new AuthInfo; + $this->bans = new Bans($this->dbConn); + $this->changelog = new Changelog($this->dbConn); + $this->comments = new Comments($this->dbConn); + $this->counters = new Counters($this->dbConn); + $this->emotes = new Emotes($this->dbConn); + $this->forum = new Forum($this->dbConn); + $this->loginAttempts = new LoginAttempts($this->dbConn); + $this->modNotes = new ModNotes($this->dbConn); + $this->news = new News($this->dbConn); + $this->profileFields = new ProfileFields($this->dbConn); + $this->recoveryTokens = new RecoveryTokens($this->dbConn); + $this->roles = new Roles($this->dbConn); + $this->sessions = new Sessions($this->dbConn); + $this->tfaSessions = new TwoFactorAuthSessions($this->dbConn); + $this->users = new Users($this->dbConn); + $this->warnings = new Warnings($this->dbConn); } public function getDbConn(): IDbConnection { @@ -177,6 +180,10 @@ class MisuzuContext { return $this->profileFields; } + public function getForum(): Forum { + return $this->forum; + } + public function createAuthTokenPacker(): AuthTokenPacker { return new AuthTokenPacker($this->config->getString('auth.secret', 'meow')); } diff --git a/src/Template.php b/src/Template.php index 90b583f..26b1038 100644 --- a/src/Template.php +++ b/src/Template.php @@ -3,6 +3,7 @@ namespace Misuzu; use InvalidArgumentException; use Twig\Environment as TwigEnvironment; +use Twig\TwigFunction; use Twig_Extensions_Extension_Date; use Twig\Loader\FilesystemLoader as TwigLoaderFilesystem; use Misuzu\MisuzuContext; @@ -29,6 +30,10 @@ final class Template { self::$loader->addPath($path); } + public static function addFunction(string $name, callable $body): void { + self::$env->addFunction(new TwigFunction($name, $body)); + } + public static function renderRaw(string $file, array $vars = []): string { if(!defined('MSZ_TPL_RENDER')) { define('MSZ_TPL_RENDER', microtime(true)); diff --git a/src/TwigMisuzu.php b/src/TwigMisuzu.php index 231b674..40ace09 100644 --- a/src/TwigMisuzu.php +++ b/src/TwigMisuzu.php @@ -23,7 +23,6 @@ final class TwigMisuzu extends AbstractExtension { new TwigFilter('html_colour', 'html_colour'), new TwigFilter('country_name', 'get_country_name'), new TwigFilter('parse_text', fn(string $text, int $parser): string => Parser::instance($parser)->parseText($text)), - new TwigFilter('perms_check', 'perms_check'), new TwigFilter('time_format', [$this, 'timeFormat']), ]; } @@ -32,8 +31,6 @@ final class TwigMisuzu extends AbstractExtension { return [ new TwigFunction('url_construct', 'url_construct'), new TwigFunction('url', 'url'), - new TwigFunction('forum_may_have_children', 'forum_may_have_children'), - new TwigFunction('forum_may_have_topics', 'forum_may_have_topics'), new TwigFunction('csrf_token', fn() => CSRF::token()), new TwigFunction('git_commit_hash', fn(bool $long = false) => GitInfo::hash($long)), new TwigFunction('git_tag', fn() => GitInfo::tag()), @@ -52,7 +49,10 @@ final class TwigMisuzu extends AbstractExtension { return compact('ndx', 'pdo', 'total'); } - public function timeFormat(DateTime|string|int $dateTime): string { + public function timeFormat(DateTime|string|int|null $dateTime): string { + if($dateTime === null) + return 'never'; + if(is_string($dateTime)) $dateTime = new DateTime($dateTime); elseif(is_int($dateTime)) diff --git a/src/Users/Assets/AssetsRoutes.php b/src/Users/Assets/AssetsRoutes.php index eb4889a..1d88490 100644 --- a/src/Users/Assets/AssetsRoutes.php +++ b/src/Users/Assets/AssetsRoutes.php @@ -1,6 +1,7 @@ bans = $bans; $this->users = $users; + $router->get('/assets/avatar', [$this, 'getAvatar']); $router->get('/assets/avatar/:filename', [$this, 'getAvatar']); + $router->get('/assets/profile-background', [$this, 'getProfileBackground']); $router->get('/assets/profile-background/:filename', [$this, 'getProfileBackground']); $router->get('/user-assets.php', [$this, 'getUserAssets']); } @@ -32,7 +35,7 @@ class AssetsRoutes { return true; } - public function getAvatar($response, $request, string $fileName) { + public function getAvatar($response, $request, string $fileName = '') { $userId = pathinfo($fileName, PATHINFO_FILENAME); $assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/no-avatar.png', MSZ_PUBLIC); @@ -46,17 +49,19 @@ class AssetsRoutes { if($userAssetInfo->isPresent()) $assetInfo = $userAssetInfo; } - } catch(RuntimeException $ex) {} + } catch(RuntimeException $ex) { + } catch(InvalidArgumentException $ex) {} return $this->serveAsset($response, $request, $assetInfo); } - public function getProfileBackground($response, $request, string $fileName) { + public function getProfileBackground($response, $request, string $fileName = '') { $userId = pathinfo($fileName, PATHINFO_FILENAME); try { $userInfo = $this->users->getUser($userId, 'id'); - } catch(RuntimeException $ex) {} + } catch(RuntimeException $ex) { + } catch(InvalidArgumentException $ex) {} if(!empty($userInfo)) { $userAssetInfo = new UserBackgroundAsset($userInfo); diff --git a/src/Users/UserInfo.php b/src/Users/UserInfo.php index f67a43a..4c3f974 100644 --- a/src/Users/UserInfo.php +++ b/src/Users/UserInfo.php @@ -6,6 +6,7 @@ use Index\TimeZoneInfo; use Index\Colour\Colour; use Index\Data\IDbResult; use Index\Net\IPAddress; +use Misuzu\Parsers\Parser; class UserInfo { private string $id; @@ -182,6 +183,18 @@ class UserInfo { return $this->aboutParser; } + public function isAboutBodyPlain(): bool { + return $this->aboutParser === Parser::PLAIN; + } + + public function isAboutBodyBBCode(): bool { + return $this->aboutParser === Parser::BBCODE; + } + + public function isAboutBodyMarkdown(): bool { + return $this->aboutParser === Parser::MARKDOWN; + } + public function hasSignatureContent(): bool { return $this->signatureContent !== null && $this->signatureContent !== ''; } @@ -194,6 +207,18 @@ class UserInfo { return $this->signatureParser; } + public function isSignatureBodyPlain(): bool { + return $this->signatureParser === Parser::PLAIN; + } + + public function isSignatureBodyBBCode(): bool { + return $this->signatureParser === Parser::BBCODE; + } + + public function isSignatureBodyMarkdown(): bool { + return $this->signatureParser === Parser::MARKDOWN; + } + public function hasBirthdate(): bool { return $this->birthdate !== null; } diff --git a/src/Users/Users.php b/src/Users/Users.php index b19edfa..df8c5ab 100644 --- a/src/Users/Users.php +++ b/src/Users/Users.php @@ -186,6 +186,7 @@ class Users { 'name' => self::GET_USER_NAME, 'email' => self::GET_USER_MAIL, 'profile' => self::GET_USER_ID | self::GET_USER_NAME, + 'search' => self::GET_USER_ID | self::GET_USER_NAME, 'login' => self::GET_USER_NAME | self::GET_USER_MAIL, 'recovery' => self::GET_USER_MAIL, ]; diff --git a/src/manage.php b/src/manage.php index 63632f7..f9d4092 100644 --- a/src/manage.php +++ b/src/manage.php @@ -33,7 +33,7 @@ function manage_get_menu(int $userId): array { $menu['News']['Categories'] = url('manage-news-categories'); if(perms_check_user(MSZ_PERMS_FORUM, $userId, MSZ_PERM_FORUM_MANAGE_FORUMS)) - $menu['Forum']['Categories'] = url('manage-forum-categories'); + $menu['Forum']['Permission Calculator'] = url('manage-forum-categories'); if(perms_check_user(MSZ_PERMS_FORUM, $userId, MSZ_PERM_FORUM_TOPIC_REDIRS)) $menu['Forum']['Topic Redirects'] = url('manage-forum-topic-redirs'); diff --git a/src/perms.php b/src/perms.php index 65c81c5..aa377a2 100644 --- a/src/perms.php +++ b/src/perms.php @@ -1,4 +1,7 @@ MSZ_PERM_COMMENTS_VOTE, ]); } + +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); + + if($user < 0 || $forum < 0) + return $perms; + + static $memo = []; + $memoId = "{$forum}-{$user}"; + + if(array_key_exists($memoId, $memo)) + return $memo[$memoId]; + + if($forum > 0) + $perms = forum_perms_get_user( + forum_get_parent_id($forum), + $user + ); + + $getPerms = \Misuzu\DB::prepare(sprintf( + ' + SELECT %s + FROM `msz_forum_permissions` + WHERE (`forum_id` = :forum_id OR `forum_id` IS NULL) + AND ( + (`user_id` IS NULL AND `role_id` IS NULL) + OR (`user_id` = :user_id_1 AND `role_id` IS NULL) + OR ( + `user_id` IS NULL + AND `role_id` IN ( + SELECT `role_id` + FROM `msz_users_roles` + WHERE `user_id` = :user_id_2 + ) + ) + ) + ', + perms_get_select(MSZ_FORUM_PERM_MODES) + )); + $getPerms->bind('forum_id', $forum); + $getPerms->bind('user_id_1', $user); + $getPerms->bind('user_id_2', $user); + + $userPerms = $getPerms->fetch(); + foreach($perms as $key => $value) + $perms[$key] |= $userPerms[$key] ?? 0; + + return $memo[$memoId] = $perms; +} + +function forum_perms_check_user( + string $prefix, + ?int $forumId, + ?int $userId, + int $perm, + bool $strict = false +): bool { + return perms_check(forum_perms_get_user($forumId, $userId)[$prefix] ?? 0, $perm, $strict); +} diff --git a/src/url.php b/src/url.php index 7fcd07b..c7664df 100644 --- a/src/url.php +++ b/src/url.php @@ -12,7 +12,7 @@ define('MSZ_URLS', [ 'info' => ['/info/'], 'search-index' => ['/search.php'], - 'search-query' => ['/search.php', ['q' => '<query>']], + 'search-query' => ['/search.php', ['q' => '<query>'], '<section>'], 'auth-login' => ['/auth/login.php', ['username' => '<username>', 'redirect' => '<redirect>']], 'auth-login-welcome' => ['/auth/login.php', ['welcome' => '1', 'username' => '<username>']], @@ -46,6 +46,7 @@ define('MSZ_URLS', [ 'forum-topic-new' => ['/forum/posting.php', ['f' => '<forum>']], 'forum-reply-new' => ['/forum/posting.php', ['t' => '<topic>']], 'forum-category' => ['/forum/forum.php', ['f' => '<forum>', 'p' => '<page>']], + 'forum-category-root' => ['/forum/index.php', [], '<forum>'], 'forum-topic' => ['/forum/topic.php', ['t' => '<topic>', 'page' => '<page>']], 'forum-topic-create' => ['/forum/posting.php', ['f' => '<forum>']], 'forum-topic-bump' => ['/forum/topic.php', ['t' => '<topic>', 'm' => 'bump', 'csrf' => '{csrf}']], diff --git a/templates/forum/forum.twig b/templates/forum/forum.twig index ddb5c5a..df3021c 100644 --- a/templates/forum/forum.twig +++ b/templates/forum/forum.twig @@ -1,27 +1,27 @@ {% extends 'forum/master.twig' %} {% from 'forum/macros.twig' import forum_category_listing, forum_topic_listing, forum_category_buttons, forum_header, forum_category_tools %} -{% set title = forum_info.forum_name %} +{% set title = forum_info.name %} {% set canonical_url = url('forum-category', { - 'forum': forum_info.forum_id, + 'forum': forum_info.id, 'page': forum_pagination.page|default(0) > 1 ? forum_pagination.page : 0, }) %} {% block content %} - {{ forum_header(forum_info.forum_name, forum_breadcrumbs, true, canonical_url, [ + {{ forum_header(forum_info.name, forum_breadcrumbs, true, canonical_url, [ { 'html': '<i class="far fa-check-circle"></i> Mark as Read', - 'url': url('forum-mark-single', {'forum': forum_info.forum_id}), + 'url': url('forum-mark-single', {'forum': forum_info.id}), 'display': forum_show_mark_as_read, 'method': 'POST', } ]) }} - {% if forum_may_have_children and forum_info.forum_subforums|length > 0 %} - {{ forum_category_listing(forum_info.forum_subforums, 'Forums') }} + {% if forum_children|length > 0 %} + {{ forum_category_listing(forum_children, 'Forums') }} {% endif %} - {% if forum_may_have_topics %} + {% if forum_info.mayHaveTopics %} {% set category_tools = forum_category_tools(forum_info, forum_perms, forum_pagination) %} {{ category_tools }} {{ forum_topic_listing(forum_topics) }} diff --git a/templates/forum/index.twig b/templates/forum/index.twig index 6d69059..a9a6d73 100644 --- a/templates/forum/index.twig +++ b/templates/forum/index.twig @@ -3,22 +3,12 @@ {% from 'forum/macros.twig' import forum_category_listing %} {% set title = 'Forum Listing' %} -{% set canonical_url = '/forum/' %} +{% set canonical_url = url('forum-index') %} {% block content %} {% if not forum_empty %} {% for category in forum_categories %} - {% if category.forum_children > 0 %} - {{ forum_category_listing( - category.forum_subforums, - category.forum_name, - category.forum_colour, - category.forum_id == constant('MSZ_FORUM_ROOT') - ? '' - : 'f' ~ category.forum_id, - category.forum_icon|default('') - ) }} - {% endif %} + {{ forum_category_listing(category) }} {% endfor %} {% if forum_show_mark_as_read %} diff --git a/templates/forum/leaderboard.twig b/templates/forum/leaderboard.twig index 2a7642a..f2e4bd2 100644 --- a/templates/forum/leaderboard.twig +++ b/templates/forum/leaderboard.twig @@ -23,7 +23,17 @@ ]) }} <div class="container forum__leaderboard__categories"> - {% for id, name in leaderboard_categories %} + <a href="{{ url('forum-leaderboard', {'mode': leaderboard_mode}) }}" class="forum__leaderboard__category{% if leaderboard_id == '' %} forum__leaderboard__category--active{% endif %}">All Time</a> + </div> + + <div class="container forum__leaderboard__categories"> + {% for id, name in leaderboard_years %} + <a href="{{ url('forum-leaderboard', {'id': id, 'mode': leaderboard_mode}) }}" class="forum__leaderboard__category{% if leaderboard_id == id %} forum__leaderboard__category--active{% endif %}">{{ name }}</a> + {% endfor %} + </div> + + <div class="container forum__leaderboard__categories"> + {% for id, name in leaderboard_months %} <a href="{{ url('forum-leaderboard', {'id': id, 'mode': leaderboard_mode}) }}" class="forum__leaderboard__category{% if leaderboard_id == id %} forum__leaderboard__category--active{% endif %}">{{ name }}</a> {% endfor %} </div> @@ -31,14 +41,14 @@ {% if leaderboard_mode == 'markdown' %} <textarea class="input__textarea forum__leaderboard__markdown">{{ leaderboard_markdown }}</textarea> {% else %} - {% for user in leaderboard_data %} - <div class="container forum__leaderboard__user forum__leaderboard__user--rank-{{ user.rank }}"> - <a href="{{ url('user-profile', {'user': user.user_id}) }}" class="forum__leaderboard__user__background"></a> + {% for ranking in leaderboard_data %} + <div class="container forum__leaderboard__user forum__leaderboard__user--rank-{{ ranking.position }}"> + <a href="{{ url('user-profile', {'user': ranking.user.id|default()}) }}" class="forum__leaderboard__user__background"></a> <div class="forum__leaderboard__user__content"> - <div class="forum__leaderboard__user__rank">{{ user.rank|number_format }}</div> - <div class="forum__leaderboard__user__avatar">{{ avatar(user.user_id, user.rank == 1 ? 50 : 40, user.username) }}</div> - <div class="forum__leaderboard__user__username">{{ user.username }}</div> - <div class="forum__leaderboard__user__posts">{{ user.posts|number_format }} posts</div> + <div class="forum__leaderboard__user__rank">{{ ranking.position|number_format }}</div> + <div class="forum__leaderboard__user__avatar">{{ avatar(ranking.user.id|default(), ranking.position == 1 ? 50 : 40, ranking.user.name|default('Deleted User')) }}</div> + <div class="forum__leaderboard__user__username">{{ ranking.user.name|default('Deleted User') }}</div> + <div class="forum__leaderboard__user__posts">{{ ranking.postsCount|number_format }} post{{ ranking.postsCount == 1 ? '' : 's' }}</div> </div> </div> {% endfor %} diff --git a/templates/forum/macros.twig b/templates/forum/macros.twig index 3d5bee1..a52a62d 100644 --- a/templates/forum/macros.twig +++ b/templates/forum/macros.twig @@ -2,10 +2,29 @@ {% from _self import forum_category_entry %} {% from 'macros.twig' import container_title %} + {% if forums.info is defined %} + {% set title = forums.info.name|default('Forums') %} + {% set icon = forums.info.iconForDisplay|default('fas fa-folder fa-fw') %} + {% set colour = forums.colour|default(null) %} + {% set id = forums.info.id is defined ? 'f' ~ forums.info.id : '' %} + {% set forums = forums.children %} + {% elseif forums.children is defined %} + {% set title = 'Forums' %} + {% set icon = 'fas fa-folder fa-fw' %} + {% set colour = null %} + {% set id = '' %} + {% set forums = forums.children %} + {% else %} + {% set title = title|default('Forums') %} + {% set icon = icon|default('fas fa-folder fa-fw') %} + {% set colour = colour|default(null) %} + {% set id = id|default('') %} + {% endif %} + <div class="container forum__categories" {% if colour is not null %}style="{{ colour|html_colour('--accent-colour') }}"{% endif %} {% if id|length > 0 %}id="{{ id }}"{% endif %}> - {{ container_title('<span class="' ~ icon|default('fas fa-folder fa-fw') ~ '"></span> ' ~ title) }} + {{ container_title('<span class="' ~ icon ~ '"></span> ' ~ title) }} {% if forums|length > 0 %} <div class="forum__categories__list"> @@ -25,15 +44,13 @@ <div class="container forum__header"> {% if breadcrumbs is iterable and breadcrumbs|length > 0 %} <div class="forum__header__breadcrumbs"> - {% for name, url in breadcrumbs %} - {% if url != breadcrumbs|first %} - <div class="forum__header__breadcrumb__separator"> - <i class="fas fa-chevron-right"></i> - </div> - {% endif %} - - {% if not (omit_last_breadcrumb|default(false) and url == breadcrumbs|last) %} - <a href="{{ url }}" class="forum__header__breadcrumb">{{ name }}</a> + <a href="{{ url('forum-index') }}" class="forum__header__breadcrumb">Forums</a> + {% for category in breadcrumbs|reverse %} + <div class="forum__header__breadcrumb__separator"> + <i class="fas fa-chevron-right"></i> + </div> + {% if not (omit_last_breadcrumb|default(false) and category == breadcrumbs|first) %} + <a href="{{ category.hasParent ? url('forum-category', {'forum': category.id}) : url('forum-category-root', {'forum': 'f' ~ category.id}) }}" class="forum__header__breadcrumb">{{ category.name }}</a> {% endif %} {% endfor %} </div> @@ -68,15 +85,23 @@ {% macro forum_category_tools(info, perms, pagination_info) %} {% from 'macros.twig' import pagination %} - {% set is_locked = info.forum_archived != 0 %} - {% set can_topic = not is_locked and perms|perms_check(constant('MSZ_FORUM_PERM_CREATE_TOPIC')) %} - {% set pag = pagination(pagination_info, url('forum-category'), null, {'f': info.forum_id}) %} + {% if info.forum_id is defined %} + {% set forum_id = info.forum_id %} + {% set is_archived = info.forum_archived != 0 %} + {% else %} + {% set forum_id = info.id %} + {% set is_archived = info.isArchived %} + {% endif %} + + {% set is_locked = is_archived %} + {% set can_topic = not is_locked and perms.can_create_topic %} + {% set pag = pagination(pagination_info, url('forum-category'), null, {'f': forum_id}) %} {% if can_topic or pag|trim|length > 0 %} <div class="container forum__actions"> <div class="forum__actions__buttons"> {% if can_topic %} - <a href="{{ url('forum-topic-new', {'forum': info.forum_id}) }}" class="input__button forum__actions__button">New Topic</a> + <a href="{{ url('forum-topic-new', {'forum': forum_id}) }}" class="input__button forum__actions__button">New Topic</a> {% endif %} </div> @@ -90,13 +115,13 @@ {% macro forum_topic_tools(info, pagination_info, can_reply) %} {% from 'macros.twig' import pagination %} - {% set pag = pagination(pagination_info, url('forum-topic'), null, {'t': info.topic_id}, 'page') %} + {% set pag = pagination(pagination_info, url('forum-topic'), null, {'t': info.id}, 'page') %} {% if can_reply or pag|trim|length > 0 %} <div class="container forum__actions"> <div class="forum__actions__buttons"> {% if can_reply %} - <a href="{{ url('forum-reply-new', {'topic': info.topic_id}) }}" class="input__button">Reply</a> + <a href="{{ url('forum-reply-new', {'topic': info.id}) }}" class="input__button">Reply</a> {% endif %} </div> @@ -109,94 +134,136 @@ {% macro forum_category_entry(forum, forum_unread, forum_icon) %} {% from 'macros.twig' import avatar %} - {% set forum_unread = forum_unread|default(forum.forum_unread|default(false)) ? 'unread' : 'read' %} - {% if forum_icon is empty %} - {% if forum.forum_icon is defined and forum.forum_icon is not empty %} - {% set forum_icon = forum.forum_icon %} - {% elseif forum.forum_archived is defined and forum.forum_archived %} - {% set forum_icon = 'fas fa-archive fa-fw' %} - {% elseif forum.forum_type is defined and forum.forum_type != constant('MSZ_FORUM_TYPE_DISCUSSION') %} - {% if forum.forum_type == constant('MSZ_FORUM_TYPE_LINK') %} - {% set forum_icon = 'fas fa-link fa-fw' %} - {% elseif forum.forum_type == constant('MSZ_FORUM_TYPE_CATEGORY') %} - {% set forum_icon = 'fas fa-folder fa-fw' %} + {% if forum.info is defined %} + {% set forum_id = forum.info.id %} + {% set forum_name = forum.info.name %} + {% set forum_desc = forum.info.description|default('') %} + {% set forum_is_link = forum.info.isLink %} + {% set forum_may_have_children = forum.info.mayHaveChildren %} + {% set forum_link_clicks = forum.info.linkClicks %} + {% set forum_count_topics = forum.info.topicsCount %} + {% set forum_count_posts = forum.info.postsCount %} + {% set forum_show_activity = forum.info.mayHaveTopics or forum.info.hasLinkClicks %} + {% set forum_unread = forum.unread %} + {% set forum_colour = forum.colour %} + + {% set forum_has_recent_post = forum.lastPost is defined %} + {% set children = forum.children %} + + {% if forum_has_recent_post %} + {% set forum_recent_post_id = forum.lastPost.info.id %} + {% set forum_recent_topic_title = forum.lastPost.topicInfo.title %} + {% set forum_recent_post_created = forum.lastPost.info.createdTime %} + + {% set forum_has_recent_post_user = forum_has_recent_post and forum.lastPost.user is defined %} + {% if forum_has_recent_post_user %} + {% set forum_recent_post_user_id = forum.lastPost.user.id %} + {% set forum_recent_post_user_name = forum.lastPost.user.name %} + {% set forum_recent_post_user_colour = '--user-colour: ' ~ forum.lastPost.colour %} {% endif %} - {% else %} - {% set forum_icon = 'fas fa-comments fa-fw' %} {% endif %} + + {% if forum_icon is empty %} + {% set forum_icon = forum.info.iconForDisplay %} + {% endif %} + {% else %} + {% set forum_id = null %} + {% set forum_name = 'Forums' %} + {% set forum_desc = null %} + {% set forum_is_link = false %} + {% set forum_may_have_children = true %} + {% set forum_count_topics = 0 %} + {% set forum_count_posts = 0 %} + {% set forum_show_activity = false %} + {% set forum_unread = false %} + {% set forum_colour = null %} + {% set forum_has_recent_post = false %} + {% set children = forum %} {% endif %} - <div class="forum__category"> - <a href="{{ url('forum-category', {'forum': forum.forum_id}) }}" class="forum__category__link"></a> + <div class="forum__category"{% if forum_colour is not null %} style="--accent-colour: {{ forum_colour }}"{% endif %}> + <a href="{{ url('forum-category', {'forum': forum_id}) }}" class="forum__category__link"></a> <div class="forum__category__container"> - <div class="forum__category__icon forum__category__icon--{{ forum_unread }}"> + <div class="forum__category__icon forum__category__icon--{{ forum_unread ? 'unread' : 'read' }}"> <span class="{{ forum_icon }}"></span> </div> <div class="forum__category__details"> <div class="forum__category__title"> - {{ forum.forum_name }} + {{ forum_name }} </div> - <div class="forum__category__description"> - {{ forum.forum_description|nl2br }} - </div> + {% if forum_desc is not null %} + <div class="forum__category__description"> + {{ forum_desc|nl2br }} + </div> + {% endif %} - {% if forum.forum_subforums is defined and forum.forum_subforums|length > 0 %} + {% if children|length > 0 %} <div class="forum__category__subforums"> - {% for subforum in forum.forum_subforums %} - <a href="{{ url('forum-category', {'forum': subforum.forum_id}) }}" - class="forum__category__subforum{% if subforum.forum_unread %} forum__category__subforum--unread{% endif %}"> - {{ subforum.forum_name }} + {% for child in children %} + {% if child.info is defined %} + {% set child_id = child.info.id %} + {% set child_name = child.info.name %} + {% set child_unread = child.unread %} + {% else %} + {% set child_id = child.forum_id %} + {% set child_name = child.forum_name %} + {% set child_unread = child.forum_unread %} + {% endif %} + + <a href="{{ url('forum-category', {'forum': child_id}) }}" + class="forum__category__subforum{% if child_unread %} forum__category__subforum--unread{% endif %}"> + {{ child_name }} </a> {% endfor %} </div> {% endif %} </div> - {% if forum.forum_type == constant('MSZ_FORUM_TYPE_LINK') %} - {% if forum.forum_link_clicks is not null %} + {% if forum_is_link %} + {% if forum_link_clicks is not null %} <div class="forum__category__stats"> - <div class="forum__category__stat" title="Clicks">{{ forum.forum_link_clicks|number_format }}</div> + <div class="forum__category__stat" title="Clicks">{{ forum_link_clicks|number_format }}</div> </div> {% endif %} - {% elseif forum_may_have_children(forum.forum_type) %} + {% elseif forum_may_have_children %} <div class="forum__category__stats"> - <div class="forum__category__stat" title="Topics">{{ forum.forum_count_topics|number_format }}</div> - <div class="forum__category__stat" title="Posts">{{ forum.forum_count_posts|number_format }}</div> + <div class="forum__category__stat" title="Topics">{{ forum_count_topics|number_format }}</div> + <div class="forum__category__stat" title="Posts">{{ forum_count_posts|number_format }}</div> </div> {% endif %} - {% if forum_may_have_topics(forum.forum_type) or forum.forum_link_clicks is not null %} - <div class="forum__category__activity{% if forum.forum_link_clicks is not null %} forum__category__activity--empty{% endif %}"> - {% if forum.forum_type != constant('MSZ_FORUM_TYPE_LINK') %} - {% if forum.recent_topic_id is not defined %} - <div class="forum__category__activity__none"> - There are no posts in this forum yet. - </div> - {% else %} + {% if forum_show_activity %} + <div class="forum__category__activity{% if forum_link_clicks is not null %} forum__category__activity--empty{% endif %}"> + {% if not forum_is_link %} + {% if forum_has_recent_post %} <div class="forum__category__activity__details"> <a class="forum__category__activity__post" - href="{{ url('forum-post', {'post': forum.recent_post_id, 'post_fragment': 'p' ~ forum.recent_post_id}) }}"> - {{ forum.recent_topic_title }} + href="{{ url('forum-post', {'post': forum_recent_post_id, 'post_fragment': 'p' ~ forum_recent_post_id}) }}"> + {{ forum_recent_topic_title }} </a> <div class="forum__category__activity__info"> - {% if forum.recent_post_user_id is not null %} - <a href="{{ url('user-profile', {'user': forum.recent_post_user_id}) }}" class="forum__category__username" - style="{{ forum.recent_post_user_colour|html_colour }}">{{ forum.recent_post_username }}</a> + {% if forum_has_recent_post_user %} + <a href="{{ url('user-profile', {'user': forum_recent_post_user_id}) }}" class="forum__category__username" + style="{{ forum_recent_post_user_colour }}">{{ forum_recent_post_user_name }}</a> {% endif %} - <time datetime="{{ forum.recent_post_created|date('c') }}" title="{{ forum.recent_post_created|date('r') }}">{{ forum.recent_post_created|time_format }}</time> + <time datetime="{{ forum_recent_post_created|date('c') }}" title="{{ forum_recent_post_created|date('r') }}">{{ forum_recent_post_created|time_format }}</time> </div> </div> - {% if forum.recent_post_user_id is not null %} - <a href="{{ url('user-profile', {'user': forum.recent_post_user_id}) }}" class="avatar forum__category__avatar"> - {{ avatar(forum.recent_post_user_id, 40, forum.recent_post_username) }} + {% if forum_has_recent_post_user %} + <a href="{{ url('user-profile', {'user': forum_recent_post_user_id}) }}" class="avatar forum__category__avatar"> + {{ avatar(forum_recent_post_user_id, 40, forum_recent_post_user_name) }} </a> {% endif %} + {% else %} + <div class="forum__category__activity__none"> + There are no posts in this forum yet. + </div> {% endif %} {% endif %} </div> @@ -221,7 +288,7 @@ {% from _self import forum_topic_notice %} {% if redirect is not empty %} {% set body %} - This topic redirects to <span class="forum__status__emphasis"><a href="{{ redirect.topic_redir_url }}" class="link">{{ redirect.topic_redir_url }}</a></span>. + This topic redirects to <span class="forum__status__emphasis"><a href="{{ redirect.linkTarget }}" class="link">{{ redirect.linkTarget }}</a></span>. {% endset %} {{ forum_topic_notice('share', body) }} {% endif %} @@ -267,33 +334,52 @@ {% macro forum_topic_entry(topic, topic_icon, topic_unread) %} {% from 'macros.twig' import avatar %} - {% set topic_unread = topic_unread|default(topic.topic_unread|default(false)) %} - {% set topic_important = topic.topic_type == constant('MSZ_TOPIC_TYPE_STICKY') or topic.topic_type == constant('MSZ_TOPIC_TYPE_ANNOUNCEMENT') or topic.topic_type == constant('MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT') %} - {% if topic_icon is null %} - {% if topic.topic_deleted is defined and topic.topic_deleted is not null %} - {% set topic_icon = 'fas fa-trash-alt' %} - {% elseif topic.topic_type is defined and topic.topic_type != constant('MSZ_TOPIC_TYPE_DISCUSSION') %} - {% if topic.topic_type == constant('MSZ_TOPIC_TYPE_ANNOUNCEMENT') or topic.topic_type == constant('MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT') %} - {% set topic_icon = 'fas fa-bullhorn' %} - {% elseif topic.topic_type == constant('MSZ_TOPIC_TYPE_STICKY') %} - {% set topic_icon = 'fas fa-thumbtack' %} - {% endif %} - {% elseif topic.topic_locked is defined and topic.topic_locked is not null %} - {% set topic_icon = 'fas fa-lock' %} - {% else %} - {% set topic_icon = (topic_unread ? 'fas' : 'far') ~ ' fa-comment' %} + {% set topic_id = topic.info.id %} + {% set topic_title = topic.info.title %} + {% set topic_participated = topic.participated %} + {% set topic_count_posts = topic.info.postsCount %} + {% set topic_count_views = topic.info.viewsCount %} + {% set topic_created = topic.info.createdTime %} + {% set topic_locked = topic.info.isLocked %} + {% set topic_deleted = topic.info.isDeleted %} + {% set topic_pages = (topic.info.postsCount / 10)|round(0, 'ceil') %} + + {% set has_topic_author = topic.user is defined %} + {% if has_topic_author %} + {% set topic_author_id = topic.user.id %} + {% set topic_author_name = topic.user.name %} + {% set topic_author_colour = '--user-colour: ' ~ topic.colour %} + {% endif %} + + {% set has_reply = topic.lastPost is defined %} + {% if has_reply %} + {% set reply_id = topic.lastPost.info.id %} + {% set reply_created = topic.lastPost.info.createdTime %} + + {% set has_reply_author = topic.lastPost.user is defined %} + {% if has_reply_author %} + {% set reply_author_id = topic.lastPost.user.id %} + {% set reply_author_name = topic.lastPost.user.name %} + {% set reply_author_colour = '--user-colour: ' ~ topic.lastPost.colour %} {% endif %} {% endif %} - <div class="forum__topic{% if topic.topic_deleted is not null %} forum__topic--deleted{% elseif topic.topic_locked is not null and not topic_important %} forum__topic--locked{% endif %}"> - <a href="{{ url('forum-topic', {'topic': topic.topic_id}) }}" class="forum__topic__link"></a> + {% set topic_unread = topic.unread %} + {% set topic_important = topic.info.isImportant %} + + {% if topic_icon is null %} + {% set topic_icon = topic.info.iconForDisplay(topic.unread) %} + {% endif %} + + <div class="forum__topic{% if topic_deleted %} forum__topic--deleted{% elseif topic_locked and not topic_important %} forum__topic--locked{% endif %}"> + <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_unread ? 'unread' : 'read' }}"> <i class="{{ topic_icon }} fa-fw"></i> - {% if topic.topic_participated %} + {% if topic_participated %} <div class="forum__topic__icon__participated" title="You have posted in this topic"></div> {% endif %} </div> @@ -301,39 +387,35 @@ <div class="forum__topic__details"> <div class="forum__topic__title"> <span class="forum__topic__title__inner"> - {{ topic.topic_title }} + {{ topic_title }} </span> </div> <div class="forum__topic__info"> - {% if topic.author_id is not null %} - by <a - href="{{ url('user-profile', {'user': topic.author_id}) }}" - class="forum__topic__username" - style="{{ topic.author_colour|html_colour }}">{{ topic.author_name }}</a>, - + {% if has_topic_author %} + by <a href="{{ url('user-profile', {'user': topic_author_id}) }}" class="forum__topic__username" style="{{ topic_author_colour }}">{{ topic_author_name }}</a>, {% endif %} - <time datetime="{{ topic.topic_created|date('c') }}" title="{{ topic.topic_created|date('r') }}">{{ topic.topic_created|time_format }}</time> + <time datetime="{{ topic_created|date('c') }}" title="{{ topic_created|date('r') }}">{{ topic_created|time_format }}</time> </div> - {% if topic.topic_pages|default(0) > 1 %} + {% if topic_pages|default(0) > 1 %} <div class="forum__topic__pagination"> - {% set topic_pages_start_end = min(3, topic.topic_pages) %} + {% set topic_pages_start_end = min(3, topic_pages) %} {% for i in 1..topic_pages_start_end %} - <a href="{{ url('forum-topic', {'topic': topic.topic_id, 'page': i}) }}" class="forum__topic__pagination__item"> + <a href="{{ url('forum-topic', {'topic': topic_id, 'page': i}) }}" class="forum__topic__pagination__item"> {{ i }} </a> {% endfor %} - {% if topic.topic_pages > 3 %} - {% if topic.topic_pages > 6 %} + {% if topic_pages > 3 %} + {% if topic_pages > 6 %} <div class="forum__topic__pagination__separator"> <i class="fas fa-ellipsis-h"></i> </div> {% endif %} - {% set topic_pages_end_start = max(4, min(topic.topic_pages, topic.topic_pages - 2)) %} - {% for i in topic_pages_end_start..topic.topic_pages %} - <a href="{{ url('forum-topic', {'topic': topic.topic_id, 'page': i}) }}" class="forum__topic__pagination__item"> + {% set topic_pages_end_start = max(4, min(topic_pages, topic_pages - 2)) %} + {% for i in topic_pages_end_start..topic_pages %} + <a href="{{ url('forum-topic', {'topic': topic_id, 'page': i}) }}" class="forum__topic__pagination__item"> {{ i }} </a> {% endfor %} @@ -343,27 +425,25 @@ </div> <div class="forum__topic__stats"> - <div class="forum__topic__stat" title="Posts">{{ topic.topic_count_posts|number_format }}</div> - <div class="forum__topic__stat" title="Views">{{ topic.topic_count_views|number_format }}</div> + <div class="forum__topic__stat" title="Posts">{{ topic_count_posts|number_format }}</div> + <div class="forum__topic__stat" title="Views">{{ topic_count_views|number_format }}</div> </div> <div class="forum__topic__activity"> <div class="forum__topic__activity__details"> - {% if topic.respondent_id is not null %} - <a href="{{ url('user-profile', {'user': topic.respondent_id}) }}" class="forum__topic__username" - style="{{ topic.respondent_colour|html_colour }}">{{ topic.respondent_name }}</a> + {% if has_reply %} + {% if has_reply_author %} + <a href="{{ url('user-profile', {'user': reply_author_id}) }}" class="forum__topic__username" style="{{ reply_author_colour }}">{{ reply_author_name }}</a> + {% endif %} + <a class="forum__topic__activity__post" href="{{ url('forum-post', {'post': reply_id, 'post_fragment': 'p' ~ reply_id}) }}"> + <time datetime="{{ reply_created|date('c') }}" title="{{ reply_created|date('r') }}">{{ reply_created|time_format }}</time> + </a> {% endif %} - - <a class="forum__topic__activity__post" - href="{{ url('forum-post', {'post': topic.response_id, 'post_fragment': 'p' ~ topic.response_id}) }}"> - <time datetime="{{ topic.response_created|date('c') }}" - title="{{ topic.response_created|date('r') }}">{{ topic.response_created|time_format }}</time> - </a> </div> - {% if topic.respondent_id is not null %} - <a href="{{ url('user-profile', {'user': topic.respondent_id}) }}" class="forum__topic__avatar"> - {{ avatar(topic.respondent_id, 30, topic.respondent_name) }} + {% if has_reply and has_reply_author %} + <a href="{{ url('user-profile', {'user': reply_author_id}) }}" class="forum__topic__avatar"> + {{ avatar(reply_author_id, 30, reply_author_name) }} </a> {% endif %} </div> @@ -381,41 +461,57 @@ {% macro forum_post_entry(post, user_id, perms) %} {% from 'macros.twig' import avatar %} - {% set is_deleted = post.post_deleted is not null %} - {% set can_post = perms|perms_check(constant('MSZ_FORUM_PERM_CREATE_POST')) %} - {% set can_edit = perms|perms_check(constant('MSZ_FORUM_PERM_EDIT_ANY_POST')) or ( - user_id == post.poster_id - and perms|perms_check(constant('MSZ_FORUM_PERM_EDIT_POST')) - ) %} - {% set can_delete = not post.is_opening_post and ( - 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') - ) - ) %} - <div class="container forum__post{% if is_deleted %} forum__post--deleted{% endif %}" id="p{{ post.post_id }}" style="{{ post.poster_colour|html_colour('--accent-colour') }}"> + {% set post_id = post.info.id %} + {% set post_created = post.info.createdTime %} + {% set post_edited = post.info.editedTime %} + {% set post_is_deleted = post.info.isDeleted %} + {% set post_is_op = post.isOriginalPost %} + {% set post_body = post.info.body|escape|parse_text(post.info.parser) %} + {% set post_is_markdown = post.info.isBodyMarkdown %} + {% set post_show_signature = post.info.shouldDisplaySignature %} + {% set post_can_be_deleted = post.info.canBeDeleted %} + {% set topic_id = post.info.topicId %} + + {% set has_author = post.user is defined %} + {% if has_author %} + {% set author_id = post.user.id %} + {% set author_name = post.user.name %} + {% set author_title = post.user.title %} + {% set author_colour = post.colour %} + {% set author_country = post.user.countryCode %} + {% set author_created = post.user.createdTime %} + {% set author_posts_count = post.postsCount %} + {% set author_is_op = post.isOriginalPoster %} + {% set signature_body = post.user.signatureContent|default('')|escape|parse_text(post.user.signatureParser) %} + {% set signature_is_markdown = post.user.isSignatureBodyMarkdown %} + {% endif %} + + {% set viewer_is_author = has_author and user_id == author_id %} + {% set can_edit = perms.can_edit_any_post|default(false) or (viewer_is_author and perms.can_edit_post|default(false)) %} + {% set can_delete = not post_is_op and (perms.can_delete_any_post|default(false) or (viewer_is_author and perms.can_delete_post|default(false) and post_can_be_deleted)) %} + + <div class="container forum__post{% if post_is_deleted %} forum__post--deleted{% endif %}" id="p{{ post_id }}"{% if author_colour is defined%} style="{{ author_colour }}"{% endif %}> <div class="forum__post__info"> <div class="forum__post__info__background"></div> <div class="forum__post__info__content"> - {% if post.poster_id is not null %} - <a class="forum__post__avatar" href="{{ url('user-profile', {'user': post.poster_id}) }}"> - {{ avatar(post.poster_id, 120, post.poster_name) }} + {% if has_author %} + <a class="forum__post__avatar" href="{{ url('user-profile', {'user': author_id}) }}"> + {{ avatar(author_id, 120, author_name) }} </a> - <a class="forum__post__username" href="{{ url('user-profile', {'user': post.poster_id}) }}">{{ post.poster_name }}</a> + <a class="forum__post__username" href="{{ url('user-profile', {'user': author_id}) }}">{{ author_name }}</a> - {% if post.poster_title|length > 0 %} - <div class="forum__post__usertitle">{{ post.poster_title }}</div> + {% if author_title|length > 0 %} + <div class="forum__post__usertitle">{{ author_title }}</div> {% endif %} <div class="forum__post__icons"> - <div class="flag flag--{{ post.poster_country|lower }}" title="{{ post.poster_country|country_name }}"></div> - <div class="forum__post__posts-count">{{ post.poster_post_count|number_format }} posts</div> + <div class="flag flag--{{ author_country|lower }}" title="{{ author_country|country_name }}"></div> + {% if author_posts_count is not null %}<div class="forum__post__posts-count">{{ author_posts_count|number_format }} posts</div>{% endif %} </div> - {% if post.is_original_poster %} + {% if author_is_op %} <div class="forum__post__badge forum__post__badge--original-poster"> <div class="forum__post__badge__desktop">Original Poster</div> <div class="forum__post__badge__mobile">OP</div> @@ -423,7 +519,7 @@ {% endif %} <div class="forum__post__joined"> - joined <time datetime="{{ post.poster_joined|date('c') }}" title="{{ post.poster_joined|date('r') }}">{{ post.poster_joined|time_format }}</time> + joined <time datetime="{{ author_created|date('c') }}" title="{{ author_created|date('r') }}">{{ author_created|time_format }}</time> </div> {% else %} <div class="forum__post__username">Deleted User</div> @@ -432,47 +528,47 @@ </div> <div class="forum__post__content"> - {% set post_link = url(post.is_opening_post ? 'forum-topic' : 'forum-post', {'topic': post.topic_id, 'post': post.post_id, 'post_fragment': 'p%d'|format(post.post_id)}) %} + {% set post_link = url(post_is_op ? 'forum-topic' : 'forum-post', {'topic': topic_id, 'post': post_id, 'post_fragment': 'p%d'|format(post_id)}) %} <div class="forum__post__details"> <a class="forum__post__datetime" href="{{ post_link }}"> - <time datetime="{{ post.post_created|date('c') }}" title="{{ post.post_created|date('r') }}">{{ post.post_created|time_format }}</time> - {% if post.post_edited is not null %} - (edited <time datetime="{{ post.post_edited|date('c') }}" title="{{ post.post_edited|date('r') }}">{{ post.post_edited|time_format }}</time>) + <time datetime="{{ post_created|date('c') }}" title="{{ post_created|date('r') }}">{{ post_created|time_format }}</time> + {% if post_edited is not null %} + (edited <time datetime="{{ post_edited|date('c') }}" title="{{ post_edited|date('r') }}">{{ post_edited|time_format }}</time>) {% endif %} </a> <a class="forum__post__id" href="{{ post_link }}"> - #{{ post.post_id }} + #{{ post_id }} </a> </div> - <div class="forum__post__text{% if post.post_parse == constant('\\Misuzu\\Parsers\\Parser::MARKDOWN') %} markdown{% endif %}"> - {{ post.post_text|escape|parse_text(post.post_parse)|raw }} + <div class="forum__post__text{% if post_is_markdown %} markdown{% endif %}"> + {{ post_body|raw }} </div> - {% if can_post or can_edit or can_delete %} + {% if perms.can_create_post|default(false) or can_edit or can_delete %} <div class="forum__post__actions"> - {% if is_deleted %} - <a href="{{ url('forum-post-restore', {'post': post.post_id}) }}" class="forum__post__action forum__post__action--restore"><i class="fas fa-magic fa-fw"></i> Restore</a> - <a href="{{ url('forum-post-nuke', {'post': post.post_id}) }}" class="forum__post__action forum__post__action--nuke"><i class="fas fa-radiation-alt fa-fw"></i> Permanently Delete</a> + {% if post_is_deleted %} + <a href="{{ url('forum-post-restore', {'post': post_id}) }}" class="forum__post__action forum__post__action--restore"><i class="fas fa-magic fa-fw"></i> Restore</a> + <a href="{{ url('forum-post-nuke', {'post': post_id}) }}" class="forum__post__action forum__post__action--nuke"><i class="fas fa-radiation-alt fa-fw"></i> Permanently Delete</a> {% else %} - {# if can_post %} - <a href="{{ url('forum-post-quote', {'post': post.post_id}) }}" class="forum__post__action forum__post__action--quote"><i class="fas fa-quote-left fa-fw"></i> Quote</a> + {# if perms.can_create_post|default(false) %} + <a href="{{ url('forum-post-quote', {'post': post_id}) }}" class="forum__post__action forum__post__action--quote"><i class="fas fa-quote-left fa-fw"></i> Quote</a> {% endif #} {% if can_edit %} - <a href="{{ url('forum-post-edit', {'post': post.post_id}) }}" class="forum__post__action forum__post__action--edit"><i class="fas fa-edit fa-fw"></i> Edit</a> + <a href="{{ url('forum-post-edit', {'post': post_id}) }}" class="forum__post__action forum__post__action--edit"><i class="fas fa-edit fa-fw"></i> Edit</a> {% endif %} {% if can_delete %} - <a href="{{ url('forum-post-delete', {'post': post.post_id}) }}" class="forum__post__action forum__post__action--delete"><i class="far fa-trash-alt fa-fw"></i> Delete</a> + <a href="{{ url('forum-post-delete', {'post': post_id}) }}" class="forum__post__action forum__post__action--delete"><i class="far fa-trash-alt fa-fw"></i> Delete</a> {% endif %} {% endif %} </div> {% endif %} - {% if post.post_display_signature and post.poster_signature_content|length > 0 %} - <div class="forum__post__signature{% if post.poster_signature_parser == constant('\\Misuzu\\Parsers\\Parser::MARKDOWN') %} markdown{% endif %}"> - {{ post.poster_signature_content|escape|parse_text(post.poster_signature_parser)|raw }} + {% if post_show_signature and signature_body is defined and signature_body|length > 0 %} + <div class="forum__post__signature{% if signature_is_markdown %} markdown{% endif %}"> + {{ signature_body|raw }} </div> {% endif %} </div> diff --git a/templates/forum/posting.twig b/templates/forum/posting.twig index f2fc5e6..e3c9d51 100644 --- a/templates/forum/posting.twig +++ b/templates/forum/posting.twig @@ -5,32 +5,32 @@ {% 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.isOriginalPost|default(false) %} {% block content %} <form method="post" action="{{ url('forum-' ~ (is_reply ? 'post' : 'topic') ~ '-create') }}" class="js-forum-posting"> - {{ input_hidden('post[' ~ (is_reply ? 'topic' : 'forum') ~ ']', is_reply ? posting_topic.topic_id : posting_forum.forum_id) }} + {{ input_hidden('post[' ~ (is_reply ? 'topic' : 'forum') ~ ']', is_reply ? posting_topic.id : posting_forum.id) }} {{ input_hidden('post[mode]', posting_mode) }} {{ input_csrf() }} {{ forum_header( is_reply and not is_opening - ? posting_topic.topic_title + ? posting_topic.title : input_text( 'post[title]', 'forum__header__input', - posting_defaults.title|default(posting_topic.topic_title|default('')), + posting_defaults.title|default(posting_topic.title|default('')), 'text', 'Enter your title here...' ), posting_breadcrumbs, false, is_reply and not is_opening - ? url('forum-topic', {'topic': posting_topic.topic_id}) + ? url('forum-topic', {'topic': posting_topic.id}) : '' ) }} {% if posting_post is defined %} - {{ input_hidden('post[id]', posting_post.post_id) }} + {{ input_hidden('post[id]', posting_post.info.id) }} {% endif %} {% if posting_notices|length > 0 %} @@ -43,21 +43,21 @@ </div> {% endif %} - <div class="container forum__post" style="{{ posting_post.poster_colour|default(posting_info.colour)|html_colour('--accent-colour') }}"> + <div class="container forum__post" style="{{ posting_post.colour|default(posting_user_colour)|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(posting_info.user_id), 120, posting_post.poster_name|default(posting_info.username)) }}</span> + <span class="forum__post__avatar">{{ avatar(posting_post.user.id|default(posting_user.id), 120, posting_post.user.name|default(posting_user.name)) }}</span> - <span class="forum__post__username">{{ posting_post.poster_name|default(posting_info.username) }}</span> + <span class="forum__post__username">{{ posting_post.user.name|default(posting_user.name) }}</span> <div class="forum__post__icons"> - <div class="flag flag--{{ posting_post.poster_country|default(posting_info.user_country)|lower }}" title="{{ posting_post.poster_country|default(posting_info.user_country)|country_name }}"></div> - <div class="forum__post__posts-count">{{ posting_post.poster_post_count|default(posting_info.user_forum_posts)|number_format }} posts</div> + <div class="flag flag--{{ posting_post.user.countryCode|default(posting_user.countryCode)|lower }}" title="{{ posting_post.user.countryCode|default(posting_user.countryCode)|country_name }}"></div> + <div class="forum__post__posts-count">{{ posting_post.postsCount|default(posting_user_posts_count)|number_format }} posts</div> </div> <div class="forum__post__joined"> - joined <time datetime="{{ posting_post.poster_joined|default(posting_info.user_created)|date('c') }}" title="{{ posting_post.poster_joined|default(posting_info.user_created)|date('r') }}">{{ posting_post.poster_joined|default(posting_info.user_created)|time_format }}</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_format }}</time> </div> </div> </div> @@ -75,7 +75,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.info.body|default('')) }}</textarea> <div class="forum__post__text js-forum-posting-preview" hidden></div> <div class="forum__post__actions forum__post__actions--bbcode" hidden> @@ -146,24 +146,23 @@ {{ input_select( 'post[parser]', constant('\\Misuzu\\Parsers\\Parser::NAMES'), - posting_defaults.parser|default(posting_post.post_parse|default(posting_info.user_post_parse|default(constant('\\Misuzu\\Parsers\\Parser::BBCODE')))), + posting_defaults.parser|default(posting_post.info.parser|default(posting_user_preferred_parser)), null, null, false, 'forum__post__dropdown js-forum-posting-parser' ) }} {% if is_opening and posting_types|length > 1 %} - {{ input_select( - 'post[type]', - posting_types, - posting_defaults.type|default(posting_topic.topic_type|default(posting_types|keys|first)), - null, null, null, 'forum__post__dropdown' - ) }} + <select class="input__select forum__post__dropdown" name="post[type]"> + {% for type_name, type_title in posting_types %} + <option value="{{ type_name }}"{% if type_name == posting_type_selected %} selected{% endif %}>{{ type_title }}</option> + {% endfor %} + </select> {% endif %} {{ input_checkbox( 'post[signature]', '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.info.shouldDisplaySignature is defined + ? posting_post.info.shouldDisplaySignature : true ) ) }} diff --git a/templates/forum/topic.twig b/templates/forum/topic.twig index d955c7a..652d0e4 100644 --- a/templates/forum/topic.twig +++ b/templates/forum/topic.twig @@ -11,50 +11,50 @@ forum_topic_redirect %} -{% set title = topic_info.topic_title %} +{% set title = topic_info.title %} {% set canonical_url = url('forum-topic', { - 'topic': topic_info.topic_id, + 'topic': topic_info.id, 'page': topic_pagination.page > 1 ? topic_pagination.page : 0, }) %} {% set forum_post_csrf = csrf_token() %} {% set topic_tools = forum_topic_tools(topic_info, topic_pagination, can_reply) %} -{% set topic_notice = forum_topic_locked(topic_info.topic_locked, topic_info.topic_archived) ~ forum_topic_redirect(topic_redir_info|default(null)) %} +{% set topic_notice = forum_topic_locked(topic_info.lockedTime, category_info.isArchived) ~ forum_topic_redirect(topic_redir_info|default(null)) %} {% set topic_actions = [ { 'html': '<i class="far fa-trash-alt fa-fw"></i> Delete', - 'url': url('forum-topic-delete', {'topic': topic_info.topic_id}), + 'url': url('forum-topic-delete', {'topic': topic_info.id}), 'display': topic_can_delete, }, { 'html': '<i class="fas fa-magic fa-fw"></i> Restore', - 'url': url('forum-topic-restore', {'topic': topic_info.topic_id}), + 'url': url('forum-topic-restore', {'topic': topic_info.id}), 'display': topic_can_nuke_or_restore, }, { 'html': '<i class="fas fa-radiation-alt fa-fw"></i> Permanently Delete', - 'url': url('forum-topic-nuke', {'topic': topic_info.topic_id}), + 'url': url('forum-topic-nuke', {'topic': topic_info.id}), 'display': topic_can_nuke_or_restore, }, { 'html': '<i class="fas fa-plus-circle fa-fw"></i> Bump', - 'url': url('forum-topic-bump', {'topic': topic_info.topic_id}), + 'url': url('forum-topic-bump', {'topic': topic_info.id}), 'display': topic_can_bump, }, { 'html': '<i class="fas fa-lock fa-fw"></i> Lock', - 'url': url('forum-topic-lock', {'topic': topic_info.topic_id}), - 'display': topic_can_lock and topic_info.topic_locked is null, + 'url': url('forum-topic-lock', {'topic': topic_info.id}), + 'display': topic_can_lock and not topic_info.isLocked, }, { 'html': '<i class="fas fa-lock-open fa-fw"></i> Unlock', - 'url': url('forum-topic-unlock', {'topic': topic_info.topic_id}), - 'display': topic_can_lock and topic_info.topic_locked is not null, + 'url': url('forum-topic-unlock', {'topic': topic_info.id}), + 'display': topic_can_lock and topic_info.isLocked, }, ] %} {% block content %} - {{ forum_header(topic_info.topic_title, topic_breadcrumbs, false, canonical_url, topic_actions) }} + {{ forum_header(topic_info.title, topic_breadcrumbs, false, canonical_url, topic_actions) }} {{ topic_notice|raw }} {{ topic_tools }} {{ forum_post_listing(topic_posts, topic_user_id, topic_perms) }} diff --git a/templates/home/search.twig b/templates/home/search.twig index 60ec6c6..86929df 100644 --- a/templates/home/search.twig +++ b/templates/home/search.twig @@ -24,7 +24,7 @@ <a href="#topics" class="search__category"> <div class="search__category__background"></div> <div class="search__category__content"> - Topics ({{ forum_topics|length|number_format }}) + Topics </div> </a> {% endif %} @@ -33,7 +33,7 @@ <a href="#posts" class="search__category"> <div class="search__category__background"></div> <div class="search__category__content"> - Posts ({{ forum_posts|length|number_format }}) + Posts </div> </a> {% endif %} @@ -42,7 +42,7 @@ <a href="#users" class="search__category"> <div class="search__category__background"></div> <div class="search__category__content"> - Users ({{ users|length|number_format }}) + Members </div> </a> {% endif %} @@ -51,7 +51,7 @@ <a href="#news" class="search__category"> <div class="search__category__background"></div> <div class="search__category__content"> - News ({{ news_posts|length|number_format }}) + News </div> </a> {% endif %} @@ -80,21 +80,30 @@ {% if forum_topics|length > 0 %} <div class="search__anchor" id="topics"></div> {{ forum_topic_listing(forum_topics, 'Topics (%d)'|format(forum_topics|length)) }} + {% if forum_topics|length >= 20 %} + <div style="text-align: center; padding: 10px;"> + <a href="{{ url('search-query', {'section': 'topics', 'query': search_merge_query({'type': 'forum:topic', 'after': forum_topics|last.info.id})}) }}" class="input__button">Load next 20 topics...</a> + </div> + {% endif %} {% endif %} {% if forum_posts|length > 0 %} <div class="search__anchor" id="posts"></div> <div class="container search__container"> {{ container_title('<i class="fas fa-comment fa-fw"></i> Posts (%s)'|format(forum_posts|length|number_format)) }} - {{ forum_post_listing(forum_posts) }} </div> + {% if forum_posts|length >= 20 %} + <div style="text-align: center; padding: 10px;"> + <a href="{{ url('search-query', {'section': 'posts', 'query': search_merge_query({'type': 'forum:post', 'after': forum_posts|last.info.id})}) }}" class="input__button">Load next 20 posts...</a> + </div> + {% endif %} {% endif %} {% if users|length > 0 %} <div class="search__anchor" id="users"></div> <div class="container search__container"> - {{ container_title('<i class="fas fa-users fa-fw"></i> Users (%s)'|format(users|length|number_format)) }} + {{ container_title('<i class="fas fa-users fa-fw"></i> Members (%s)'|format(users|length|number_format)) }} <div class="userlist userlist--search"> {% for user in users %} diff --git a/templates/manage/forum/forum.twig b/templates/manage/forum/forum.twig deleted file mode 100644 index 77d1ec8..0000000 --- a/templates/manage/forum/forum.twig +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'manage/users/master.twig' %} -{% from 'macros.twig' import container_title %} -{% from 'manage/macros.twig' import permissions_table %} -{% from '_layout/input.twig' import input_hidden, input_csrf, input_select %} - -{% block manage_content %} - <div class="container"> - {{ container_title(forum.forum_name) }} - there's nothing here go away - </div> -{% endblock %} diff --git a/templates/manage/forum/listing.twig b/templates/manage/forum/listing.twig index 1c17c6b..5435c6c 100644 --- a/templates/manage/forum/listing.twig +++ b/templates/manage/forum/listing.twig @@ -4,17 +4,7 @@ {% block manage_content %} <div class="container container--lazy"> - {{ container_title('Forum Listing') }} - - <div class="container__content"> - {% for forum in forums %} - <a href="{{ url('manage-forum-category', {'forum': forum.forum_id}) }}" class="warning__link">{{ forum.forum_name }}</a><br> - {% endfor %} - </div> - </div> - - <div class="container container--lazy"> - {{ container_title('Permission Calculator') }} + {{ container_title('<i class="fas fa-calculator fa-fw"></i> Permission Calculator') }} Remove this when the permission manager exists. diff --git a/templates/manage/forum/redirs.twig b/templates/manage/forum/redirs.twig index ec757a2..5df1dca 100644 --- a/templates/manage/forum/redirs.twig +++ b/templates/manage/forum/redirs.twig @@ -51,23 +51,23 @@ {% for redir in manage_redirs %} <tr class="manage-list-setting"> <td class="manage-list-setting-key"> - <div class="manage-list-setting-key-text">{{ redir.topic_id }}</div> + <div class="manage-list-setting-key-text">{{ redir.topicId }}</div> </td> <td class="manage-list-setting-key"> - <div class="manage-list-setting-key-text">{{ redir.user_id }}</div> + <div class="manage-list-setting-key-text">{{ redir.hasUserId ? redir.userId : 'System' }}</div> </td> <td class="manage-list-setting-value"> - <div class="manage-list-setting-value-text">{{ redir.topic_redir_url }}</div> + <div class="manage-list-setting-value-text">{{ redir.linkTarget }}</div> </td> <td class="manage-list-setting-value"> <div class="manage-list-setting-value-text"> - <time datetime="{{ redir.topic_redir_created|date('c') }}" title="{{ redir.topic_redir_created|date('r') }}"> - {{ redir.topic_redir_created|time_format }} + <time datetime="{{ redir.createdTime|date('c') }}" title="{{ redir.createdTime|date('r') }}"> + {{ redir.createdTime|time_format }} </time> </div> </td> <td class="manage-list-setting-options"> - <a class="input__button input__button--autosize input__button--destroy" href="{{ url('manage-forum-topic-redirs-nuke', {'topic': redir.topic_id}) }}" title="Delete"><i class="fas fa-times fa-fw"></i></a> + <a class="input__button input__button--autosize input__button--destroy" href="{{ url('manage-forum-topic-redirs-nuke', {'topic': redir.topicId}) }}" title="Delete"><i class="fas fa-times fa-fw"></i></a> </td> </tr> {% endfor %} diff --git a/templates/profile/index.twig b/templates/profile/index.twig index ce7aa97..69e9156 100644 --- a/templates/profile/index.twig +++ b/templates/profile/index.twig @@ -79,7 +79,7 @@ {% set show_profile_fields = not profile_is_guest and (profile_is_editing ? perms.edit_profile : profile_fields_display_values|default([]) is not empty) %} {% set show_background_settings = profile_is_editing and perms.edit_background %} {% set show_birthdate = profile_is_editing and perms.edit_birthdate %} - {% set show_active_forum_info = not profile_is_editing and (profile_active_category_info.forum_id|default(0) > 0 or profile_active_topic_info.topic_id|default(0) > 0) %} + {% set show_active_forum_info = not profile_is_deleted and not profile_is_editing and (profile_active_category_info is not empty or profile_active_topic_info.topic_id|default(0) > 0) %} {% set show_warnings = profile_warnings is defined and profile_warnings|length > 0 %} {% set show_sidebar = (not profile_is_banned or profile_can_edit) and (profile_is_guest or show_profile_fields or show_background_settings or show_birthdate or show_active_forum_info or show_warnings) %} @@ -144,41 +144,26 @@ <div class="profile__forum-activity__content"> {% if profile_active_category_info is not empty %} <div class="profile__forum-activity__category"> - {% set forum = profile_active_category_info %} - {% if forum.forum_icon is defined and forum.forum_icon is not empty %} - {% set forum_icon = forum.forum_icon %} - {% elseif forum.forum_archived is defined and forum.forum_archived %} - {% set forum_icon = 'fas fa-archive fa-fw' %} - {% elseif forum.forum_type is defined and forum.forum_type != constant('MSZ_FORUM_TYPE_DISCUSSION') %} - {% if forum.forum_type == constant('MSZ_FORUM_TYPE_LINK') %} - {% set forum_icon = 'fas fa-link fa-fw' %} - {% elseif forum.forum_type == constant('MSZ_FORUM_TYPE_CATEGORY') %} - {% set forum_icon = 'fas fa-folder fa-fw' %} - {% endif %} - {% else %} - {% set forum_icon = 'fas fa-comments fa-fw' %} - {% endif %} - <div class="profile__forum-activity__leader"> Most active category </div> <div class="forum__category"> - <a href="{{ url('forum-category', {'forum': forum.forum_id}) }}" class="forum__category__link"></a> + <a href="{{ url('forum-category', {'forum': profile_active_category_info.id}) }}" class="forum__category__link"></a> <div class="forum__category__container"> <div class="forum__category__icon"> - <span class="{{ forum_icon }}"></span> + <span class="{{ profile_active_category_info.iconForDisplay }}"></span> </div> <div class="forum__category__details"> <div class="forum__category__title"> - {{ forum.forum_name }} + {{ profile_active_category_info.name }} </div> <div class="forum__category__description"> - {{ profile_active_category_stats.post_count|number_format }} post{{ profile_active_category_stats.post_count == 1 ? '' : 's' }} - / {{ ((profile_active_category_stats.post_count / profile_stats.forum_post_count) * 100)|number_format(2) }}% of total posts + {{ profile_active_category_stats.postCount|number_format }} post{{ profile_active_category_stats.postCount == 1 ? '' : 's' }} + / {{ ((profile_active_category_stats.postCount / profile_stats.forum_post_count) * 100)|number_format(2) }}% of total posts </div> </div> </div> @@ -187,43 +172,28 @@ {% endif %} {% if profile_active_topic_info is not empty %} <div class="profile__forum-activity__topic"> - {% set topic = profile_active_topic_info %} - {% if topic.topic_deleted is defined and topic.topic_deleted is not null %} - {% set topic_icon = 'fas fa-trash-alt' %} - {% elseif topic.topic_type is defined and topic.topic_type != constant('MSZ_TOPIC_TYPE_DISCUSSION') %} - {% if topic.topic_type == constant('MSZ_TOPIC_TYPE_ANNOUNCEMENT') or topic.topic_type == constant('MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT') %} - {% set topic_icon = 'fas fa-bullhorn' %} - {% elseif topic.topic_type == constant('MSZ_TOPIC_TYPE_STICKY') %} - {% set topic_icon = 'fas fa-thumbtack' %} - {% endif %} - {% elseif topic.topic_locked is defined and topic.topic_locked is not null %} - {% set topic_icon = 'fas fa-lock' %} - {% else %} - {% set topic_icon = 'fas fa-comment' %} - {% endif %} - <div class="profile__forum-activity__leader"> Most active topic </div> - <div class="forum__topic{% if topic.topic_locked is not null %} forum__topic--locked{% endif %}"> - <a href="{{ url('forum-topic', {'topic': topic.topic_id}) }}" class="forum__topic__link"></a> + <div class="forum__topic{% if profile_active_topic_info.isLocked %} forum__topic--locked{% endif %}"> + <a href="{{ url('forum-topic', {'topic': profile_active_topic_info.id}) }}" class="forum__topic__link"></a> <div class="forum__topic__container"> <div class="forum__topic__icon"> - <i class="{{ topic_icon }} fa-fw"></i> + <i class="{{ profile_active_topic_info.iconForDisplay }} fa-fw"></i> </div> <div class="forum__topic__details"> <div class="forum__topic__title"> <span class="forum__topic__title__inner"> - {{ topic.topic_title }} + {{ profile_active_topic_info.title }} </span> </div> <div class="forum__topic__info"> - {{ profile_active_topic_stats.post_count|number_format }} post{{ profile_active_topic_stats.post_count == 1 ? '' : 's' }} - / {{ ((profile_active_topic_stats.post_count / profile_stats.forum_post_count) * 100)|number_format(2) }}% of total posts + {{ profile_active_topic_stats.postCount|number_format }} post{{ profile_active_topic_stats.postCount == 1 ? '' : 's' }} + / {{ ((profile_active_topic_stats.postCount / profile_stats.forum_post_count) * 100)|number_format(2) }}% of total posts </div> </div> </div> diff --git a/templates/profile/posts.twig b/templates/profile/posts.twig deleted file mode 100644 index bbb02b6..0000000 --- a/templates/profile/posts.twig +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'profile/master.twig' %} -{% from 'macros.twig' import pagination %} -{% from 'forum/macros.twig' import forum_post_listing %} - -{% block content %} - <div class="profile"> - {% include 'profile/_layout/header.twig' %} - - {% set sp = profile_posts_pagination.pages > 1 - ? '<div class="container profile__pagination">' ~ pagination(profile_posts_pagination, canonical_url) ~ '</div>' - : '' %} - - {% if sp is not empty %} - {{ sp|raw }} - {% endif %} - - {{ forum_post_listing(profile_posts) }} - - {% if sp is not empty %} - {{ sp|raw }} - {% endif %} - </div> -{% endblock %} diff --git a/templates/profile/topics.twig b/templates/profile/topics.twig deleted file mode 100644 index 1277302..0000000 --- a/templates/profile/topics.twig +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'profile/master.twig' %} -{% from 'macros.twig' import pagination %} -{% from 'forum/macros.twig' import forum_topic_listing %} - -{% block content %} - <div class="profile"> - {% include 'profile/_layout/header.twig' %} - - {% set sp = profile_topics_pagination.pages > 1 - ? '<div class="container profile__pagination">' ~ pagination(profile_topics_pagination, canonical_url) ~ '</div>' - : '' %} - - {% if sp is not empty %} - {{ sp|raw }} - {% endif %} - - {{ forum_topic_listing(profile_topics) }} - - {% if sp is not empty %} - {{ sp|raw }} - {% endif %} - </div> -{% endblock %} diff --git a/tools/cron b/tools/cron index cd0fca3..bfadfb1 100755 --- a/tools/cron +++ b/tools/cron @@ -74,8 +74,9 @@ msz_sched_task_sql('Remove stale forum tracking entries.', false, msz_sched_task_sql('Synchronise forum_id.', true, 'UPDATE msz_forum_posts AS p INNER JOIN msz_forum_topics AS t ON t.topic_id = p.topic_id SET p.forum_id = t.forum_id'); -msz_sched_task_func('Recount forum topics and posts.', true, - function() { forum_count_synchronise(); }); +msz_sched_task_func('Recount forum topics and posts.', true, function() use ($msz) { + $msz->getForum()->syncForumCounters(); +}); msz_sched_task_sql('Clean up expired 2fa tokens.', false, 'DELETE FROM msz_auth_tfa WHERE tfa_created < NOW() - INTERVAL 15 MINUTE');