Rewrote forum backend.

This commit is contained in:
flash 2023-08-28 01:17:34 +00:00
parent fb41c71ee9
commit 39c6269cf3
48 changed files with 3798 additions and 3032 deletions

24
composer.lock generated
View file

@ -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": [],

View file

@ -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);

View file

@ -1,77 +1,199 @@
<?php
namespace Misuzu;
$forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0;
$forumId = max($forumId, 0);
use stdClass;
use RuntimeException;
if($forumId === 0) {
url_redirect('forum-index');
exit;
}
$forum = $msz->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,
]);

View file

@ -1,57 +1,207 @@
<?php
namespace Misuzu;
$indexMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0;
use stdClass;
use RuntimeException;
$forum = $msz->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,
]);

View file

@ -1,63 +1,116 @@
<?php
namespace Misuzu;
use RuntimeException;
if(!$msz->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 = <<<MD
# {$leaderboardName}
# {$name}
| Rank | Usename | Post count |
| ----:|:------- | ----------:|
MD;
foreach($leaderboard as $user) {
$markdown .= sprintf("| %s | [%s](%s%s) | %s |\r\n", $user['rank'], $user['username'], url_prefix(false), url('user-profile', ['user' => $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,
]);

View file

@ -1,6 +1,10 @@
<?php
namespace Misuzu;
use RuntimeException;
$forum = $msz->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;
}

View file

@ -1,6 +1,10 @@
<?php
namespace Misuzu;
use stdClass;
use RuntimeException;
use Index\DateTime;
use Misuzu\Forum\ForumTopicInfo;
use Misuzu\Parsers\Parser;
if(!$msz->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,

View file

@ -1,43 +1,76 @@
<?php
namespace Misuzu;
use stdClass;
use RuntimeException;
$forum = $msz->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,
]);

View file

@ -1,22 +0,0 @@
<?php
namespace Misuzu;
if(!$msz->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'));

View file

@ -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'));

View file

@ -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,

View file

@ -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))

View file

@ -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,
]);

View file

@ -1,7 +1,9 @@
<?php
namespace Misuzu;
use stdClass;
use RuntimeException;
use Index\XArray;
use Misuzu\Comments\CommentsCategory;
if(!$msz->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', [

1558
src/Forum/Forum.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,194 @@
<?php
namespace Misuzu\Forum;
use Index\DateTime;
use Index\Colour\Colour;
use Index\Data\IDbResult;
class ForumCategoryInfo {
// should the types just be replaced with flags indicating what's allowed?
public const TYPE_DISCUSSION = 0;
public const TYPE_LISTING = 1;
public const TYPE_LINK = 2;
public const TYPE_ALIASES = [
'discussion' => 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;
}
}

135
src/Forum/ForumPostInfo.php Normal file
View file

@ -0,0 +1,135 @@
<?php
namespace Misuzu\Forum;
use Index\DateTime;
use Index\Data\IDbResult;
use Index\Net\IPAddress;
use Misuzu\Parsers\Parser;
class ForumPostInfo {
private string $id;
private string $topicId;
private string $categoryId;
private ?string $userId;
private string $remoteAddr;
private string $body;
private int $parser;
private bool $displaySignature;
private int $created;
private ?int $edited;
private ?int $deleted;
public function __construct(IDbResult $result) {
$this->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);
}
}

View file

@ -0,0 +1,170 @@
<?php
namespace Misuzu\Forum;
use Index\DateTime;
use Index\Data\IDbResult;
class ForumTopicInfo {
public const TYPE_DISCUSSION = 0;
public const TYPE_STICKY = 1;
public const TYPE_ANNOUNCE = 2;
public const TYPE_GLOBAL = 3;
public const TYPE_ALIASES = [
'discussion' => 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);
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Misuzu\Forum;
use Index\DateTime;
use Index\Data\IDbResult;
class ForumTopicRedirectInfo {
private string $topicId;
private ?string $userId;
private string $link;
private string $created;
public function __construct(IDbResult $result) {
$this->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);
}
}

View file

@ -1,586 +0,0 @@
<?php
define('MSZ_FORUM_TYPE_DISCUSSION', 0);
define('MSZ_FORUM_TYPE_CATEGORY', 1);
define('MSZ_FORUM_TYPE_LINK', 2);
define('MSZ_FORUM_TYPES', [
MSZ_FORUM_TYPE_DISCUSSION,
MSZ_FORUM_TYPE_CATEGORY,
MSZ_FORUM_TYPE_LINK,
]);
define('MSZ_FORUM_MAY_HAVE_CHILDREN', [
MSZ_FORUM_TYPE_DISCUSSION,
MSZ_FORUM_TYPE_CATEGORY,
]);
define('MSZ_FORUM_MAY_HAVE_TOPICS', [
MSZ_FORUM_TYPE_DISCUSSION,
]);
define('MSZ_FORUM_ROOT', 0);
define('MSZ_FORUM_ROOT_DATA', [ // should be compatible with the data fetched in forum_get_root_categories
'forum_id' => 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;
}

View file

@ -1,98 +0,0 @@
<?php
define('MSZ_FORUM_LEADERBOARD_START_YEAR', 2018);
define('MSZ_FORUM_LEADERBOARD_START_MONTH', 12);
define('MSZ_FORUM_LEADERBOARD_CATEGORY_ALL', 0);
function forum_leaderboard_year_valid(?int $year): bool {
return !is_null($year) && $year >= 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;
}

View file

@ -1,212 +0,0 @@
<?php
define('MSZ_FORUM_PERMS_GENERAL', 'forum');
define('MSZ_FORUM_PERM_LIST_FORUM', 0x00000001); // can see stats, but will get error when trying to view
define('MSZ_FORUM_PERM_VIEW_FORUM', 0x00000002);
define('MSZ_FORUM_PERM_CREATE_TOPIC', 0x00000400);
//define('MSZ_FORUM_PERM_DELETE_TOPIC', 0x00000800); // use MSZ_FORUM_PERM_DELETE_ANY_POST instead
define('MSZ_FORUM_PERM_MOVE_TOPIC', 0x00001000);
define('MSZ_FORUM_PERM_LOCK_TOPIC', 0x00002000);
define('MSZ_FORUM_PERM_STICKY_TOPIC', 0x00004000);
define('MSZ_FORUM_PERM_ANNOUNCE_TOPIC', 0x00008000);
define('MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC', 0x00010000);
define('MSZ_FORUM_PERM_BUMP_TOPIC', 0x00020000);
//define('MSZ_FORUM_PERM_PRIORITY_VOTE', 0x00040000); // feature postponed, perhaps reuse if it makes sense to
define('MSZ_FORUM_PERM_CREATE_POST', 0x00100000);
define('MSZ_FORUM_PERM_EDIT_POST', 0x00200000);
define('MSZ_FORUM_PERM_EDIT_ANY_POST', 0x00400000);
define('MSZ_FORUM_PERM_DELETE_POST', 0x00800000);
define('MSZ_FORUM_PERM_DELETE_ANY_POST', 0x01000000);
// shorthands, never use these to SET!!!!!!!
define('MSZ_FORUM_PERM_SET_READ', MSZ_FORUM_PERM_LIST_FORUM | MSZ_FORUM_PERM_VIEW_FORUM);
define(
'MSZ_FORUM_PERM_SET_WRITE',
MSZ_FORUM_PERM_CREATE_TOPIC
| MSZ_FORUM_PERM_MOVE_TOPIC
| MSZ_FORUM_PERM_LOCK_TOPIC
| MSZ_FORUM_PERM_STICKY_TOPIC
| MSZ_FORUM_PERM_ANNOUNCE_TOPIC
| MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC
| MSZ_FORUM_PERM_CREATE_POST
| MSZ_FORUM_PERM_EDIT_POST
| MSZ_FORUM_PERM_EDIT_ANY_POST
| MSZ_FORUM_PERM_DELETE_POST
| MSZ_FORUM_PERM_DELETE_ANY_POST
| MSZ_FORUM_PERM_BUMP_TOPIC
);
define('MSZ_FORUM_PERM_MODES', [
MSZ_FORUM_PERMS_GENERAL,
]);
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_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);
}

View file

@ -1,363 +0,0 @@
<?php
define('MSZ_FORUM_POSTS_PER_PAGE', 10);
function forum_post_create(
int $topicId,
int $forumId,
int $userId,
string $ipAddress,
string $text,
int $parser = \Misuzu\Parsers\Parser::PLAIN,
bool $displaySignature = true
): int {
$createPost = \Misuzu\DB::prepare('
INSERT INTO `msz_forum_posts`
(`topic_id`, `forum_id`, `user_id`, `post_ip`, `post_text`, `post_parse`, `post_display_signature`)
VALUES
(:topic_id, :forum_id, :user_id, INET6_ATON(:post_ip), :post_text, :post_parse, :post_display_signature)
');
$createPost->bind('topic_id', $topicId);
$createPost->bind('forum_id', $forumId);
$createPost->bind('user_id', $userId);
$createPost->bind('post_ip', $ipAddress);
$createPost->bind('post_text', $text);
$createPost->bind('post_parse', $parser);
$createPost->bind('post_display_signature', $displaySignature ? 1 : 0);
return $createPost->execute() ? \Misuzu\DB::lastId() : 0;
}
function forum_post_update(
int $postId,
string $ipAddress,
string $text,
int $parser = \Misuzu\Parsers\Parser::PLAIN,
bool $displaySignature = true,
bool $bumpUpdate = true
): bool {
if($postId < 1) {
return false;
}
$updatePost = \Misuzu\DB::prepare('
UPDATE `msz_forum_posts`
SET `post_ip` = INET6_ATON(:post_ip),
`post_text` = :post_text,
`post_parse` = :post_parse,
`post_display_signature` = :post_display_signature,
`post_edited` = IF(:bump, NOW(), `post_edited`)
WHERE `post_id` = :post_id
');
$updatePost->bind('post_id', $postId);
$updatePost->bind('post_ip', $ipAddress);
$updatePost->bind('post_text', $text);
$updatePost->bind('post_parse', $parser);
$updatePost->bind('post_display_signature', $displaySignature ? 1 : 0);
$updatePost->bind('bump', $bumpUpdate ? 1 : 0);
return $updatePost->execute();
}
function forum_post_find(int $postId, int $userId): array {
$getPostInfo = \Misuzu\DB::prepare(sprintf(
'
SELECT
p.`post_id`, p.`topic_id`,
(
SELECT COUNT(`post_id`)
FROM `msz_forum_posts`
WHERE `topic_id` = p.`topic_id`
AND `post_id` < p.`post_id`
AND `post_deleted` IS NULL
ORDER BY `post_id`
) as `preceeding_post_count`,
(
SELECT COUNT(`post_id`)
FROM `msz_forum_posts`
WHERE `topic_id` = p.`topic_id`
AND `post_id` < p.`post_id`
AND `post_deleted` IS NOT NULL
ORDER BY `post_id`
) as `preceeding_post_deleted_count`
FROM `msz_forum_posts` AS p
WHERE p.`post_id` = :post_id
'));
$getPostInfo->bind('post_id', $postId);
return $getPostInfo->fetch();
}
function forum_post_get(int $postId, bool $allowDeleted = false): array {
// 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();
}

View file

@ -1,719 +0,0 @@
<?php
define('MSZ_TOPIC_TYPE_DISCUSSION', 0);
define('MSZ_TOPIC_TYPE_STICKY', 1);
define('MSZ_TOPIC_TYPE_ANNOUNCEMENT', 2);
define('MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT', 3);
define('MSZ_TOPIC_TYPES', [
MSZ_TOPIC_TYPE_DISCUSSION,
MSZ_TOPIC_TYPE_STICKY,
MSZ_TOPIC_TYPE_ANNOUNCEMENT,
MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT,
]);
define('MSZ_TOPIC_TYPE_ORDER', [ // in which order to display topics, only add types here that should appear above others
MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT,
MSZ_TOPIC_TYPE_ANNOUNCEMENT,
MSZ_TOPIC_TYPE_STICKY,
]);
function forum_topic_is_valid_type(int $type): bool {
return in_array($type, MSZ_TOPIC_TYPES, true);
}
function forum_topic_create(
int $forumId,
int $userId,
string $title,
int $type = MSZ_TOPIC_TYPE_DISCUSSION
): int {
if(empty($title) || !forum_topic_is_valid_type($type)) {
return 0;
}
$createTopic = \Misuzu\DB::prepare('
INSERT INTO `msz_forum_topics`
(`forum_id`, `user_id`, `topic_title`, `topic_type`)
VALUES
(:forum_id, :user_id, :topic_title, :topic_type)
');
$createTopic->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();
}

View file

@ -1,33 +0,0 @@
<?php
define('MSZ_TOPIC_TITLE_LENGTH_MIN', 3);
define('MSZ_TOPIC_TITLE_LENGTH_MAX', 100);
define('MSZ_POST_TEXT_LENGTH_MIN', 1);
define('MSZ_POST_TEXT_LENGTH_MAX', 60000);
function forum_validate_title(string $title): string {
$length = mb_strlen(trim($title));
if($length < MSZ_TOPIC_TITLE_LENGTH_MIN) {
return 'too-short';
}
if($length > 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 '';
}

View file

@ -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'));
}

View file

@ -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));

View file

@ -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))

View file

@ -1,6 +1,7 @@
<?php
namespace Misuzu\Users\Assets;
use InvalidArgumentException;
use RuntimeException;
use Index\Routing\IRouter;
use Misuzu\Auth\AuthInfo;
@ -18,7 +19,9 @@ class AssetsRoutes {
$this->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);

View file

@ -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;
}

View file

@ -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,
];

View file

@ -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');

View file

@ -1,4 +1,7 @@
<?php
define('MSZ_PERMS_ALLOW', 'allow');
define('MSZ_PERMS_DENY', 'deny');
define('MSZ_PERMS_GENERAL', 'general');
define('MSZ_PERM_GENERAL_CAN_MANAGE', 0x00000001);
define('MSZ_PERM_GENERAL_VIEW_LOGS', 0x00000002);
@ -54,8 +57,30 @@ define('MSZ_PERM_MODES', [
MSZ_PERMS_NEWS, MSZ_PERMS_FORUM, MSZ_PERMS_COMMENTS,
]);
define('MSZ_PERMS_ALLOW', 'allow');
define('MSZ_PERMS_DENY', 'deny');
define('MSZ_FORUM_PERMS_GENERAL', 'forum');
define('MSZ_FORUM_PERM_LIST_FORUM', 0x00000001); // can see stats, but will get error when trying to view
define('MSZ_FORUM_PERM_VIEW_FORUM', 0x00000002);
define('MSZ_FORUM_PERM_CREATE_TOPIC', 0x00000400);
//define('MSZ_FORUM_PERM_DELETE_TOPIC', 0x00000800); // use MSZ_FORUM_PERM_DELETE_ANY_POST instead
define('MSZ_FORUM_PERM_MOVE_TOPIC', 0x00001000);
define('MSZ_FORUM_PERM_LOCK_TOPIC', 0x00002000);
define('MSZ_FORUM_PERM_STICKY_TOPIC', 0x00004000);
define('MSZ_FORUM_PERM_ANNOUNCE_TOPIC', 0x00008000);
define('MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC', 0x00010000);
define('MSZ_FORUM_PERM_BUMP_TOPIC', 0x00020000);
//define('MSZ_FORUM_PERM_PRIORITY_VOTE', 0x00040000); // feature postponed, perhaps reuse if it makes sense to
define('MSZ_FORUM_PERM_CREATE_POST', 0x00100000);
define('MSZ_FORUM_PERM_EDIT_POST', 0x00200000);
define('MSZ_FORUM_PERM_EDIT_ANY_POST', 0x00400000);
define('MSZ_FORUM_PERM_DELETE_POST', 0x00800000);
define('MSZ_FORUM_PERM_DELETE_ANY_POST', 0x01000000);
define('MSZ_FORUM_PERM_MODES', [
MSZ_FORUM_PERMS_GENERAL,
]);
function perms_get_keys(array $modes = MSZ_PERM_MODES): array {
$perms = [];
@ -278,3 +303,81 @@ function perms_for_comments(string|int $userId): array {
'can_vote' => 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);
}

View file

@ -12,7 +12,7 @@ define('MSZ_URLS', [
'info' => ['/info/<title>'],
'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}']],

View file

@ -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) }}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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>

View file

@ -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
)
) }}

View file

@ -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) }}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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.

View file

@ -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 %}

View file

@ -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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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');