// whoa i can't believe it's more progress!

This commit is contained in:
flash 2020-10-21 22:33:21 +00:00
parent ceb05fc3f7
commit 0633a48f09
17 changed files with 456 additions and 759 deletions

View file

@ -76,9 +76,6 @@ require_once 'src/perms.php';
require_once 'src/manage.php'; require_once 'src/manage.php';
require_once 'src/url.php'; require_once 'src/url.php';
require_once 'src/Forum/perms.php'; require_once 'src/Forum/perms.php';
require_once 'src/Forum/forum.php';
require_once 'src/Forum/post.php';
require_once 'src/Forum/topic.php';
$dbConfig = parse_ini_file(MSZ_CONFIG . '/config.ini', true, INI_SCANNER_TYPED); $dbConfig = parse_ini_file(MSZ_CONFIG . '/config.ini', true, INI_SCANNER_TYPED);

View file

@ -2,6 +2,8 @@
namespace Misuzu; namespace Misuzu;
use Misuzu\AuditLog; use Misuzu\AuditLog;
use Misuzu\Forum\ForumPost;
use Misuzu\Forum\ForumPostNotFoundException;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserSession; use Misuzu\Users\UserSession;
@ -55,59 +57,34 @@ if($isXHR) {
header(CSRF::header()); header(CSRF::header());
} }
$postInfo = forum_post_get($postId, true); try {
$perms = empty($postInfo) $postInfo = ForumPost::byId($postId);
? 0 $perms = forum_perms_get_user($postInfo->getCategoryId(), $currentUserId)[MSZ_FORUM_PERMS_GENERAL];
: forum_perms_get_user($postInfo['forum_id'], $currentUserId)[MSZ_FORUM_PERMS_GENERAL]; } catch(ForumPostNotFoundException $ex) {
$postInfo = null;
$perms = 0;
}
switch($postMode) { switch($postMode) {
case 'delete': case 'delete':
$canDelete = forum_post_can_delete($postInfo, $currentUserId); $canDeleteCodes = [
$canDeleteMsg = ''; 'view' => 404,
$responseCode = 200; 'deleted' => 404,
'owner' => 403,
'age' => 403,
'permission' => 403,
'' => 200,
];
$canDelete = $postInfo->canBeDeleted($currentUser);
$canDeleteMsg = ForumPost::canBeDeletedErrorString($canDelete);
$responseCode = $canDeleteCodes[$canDelete] ?? 500;
switch($canDelete) { if($canDelete !== '') {
case MSZ_E_FORUM_POST_DELETE_USER: // i don't think this is ever reached but we may as well have it
$responseCode = 401;
$canDeleteMsg = 'You must be logged in to delete posts.';
break;
case MSZ_E_FORUM_POST_DELETE_POST:
$responseCode = 404;
$canDeleteMsg = "This post doesn't exist.";
break;
case MSZ_E_FORUM_POST_DELETE_DELETED:
$responseCode = 404;
$canDeleteMsg = 'This post has already been marked as deleted.';
break;
case MSZ_E_FORUM_POST_DELETE_OWNER:
$responseCode = 403;
$canDeleteMsg = 'You can only delete your own posts.';
break;
case MSZ_E_FORUM_POST_DELETE_OLD:
$responseCode = 401;
$canDeleteMsg = 'This post has existed for too long. Ask a moderator to remove if it absolutely necessary.';
break;
case MSZ_E_FORUM_POST_DELETE_PERM:
$responseCode = 401;
$canDeleteMsg = 'You are not allowed to delete posts.';
break;
case MSZ_E_FORUM_POST_DELETE_OP:
$responseCode = 403;
$canDeleteMsg = 'This is the opening post of a topic, it may not be deleted without deleting the entire topic as well.';
break;
case MSZ_E_FORUM_POST_DELETE_OK:
break;
default:
$responseCode = 500;
$canDeleteMsg = sprintf('Unknown error \'%d\'', $canDelete);
}
if($canDelete !== MSZ_E_FORUM_POST_DELETE_OK) {
if($isXHR) { if($isXHR) {
http_response_code($responseCode); http_response_code($responseCode);
echo json_encode([ echo json_encode([
'success' => false, 'success' => false,
'post_id' => $postInfo['post_id'], 'post_id' => $postInfo->getId(),
'code' => $canDelete, 'code' => $canDelete,
'message' => $canDeleteMsg, 'message' => $canDeleteMsg,
]); ]);
@ -121,17 +98,17 @@ switch($postMode) {
if(!$isXHR) { if(!$isXHR) {
if($postRequestVerified && !$submissionConfirmed) { if($postRequestVerified && !$submissionConfirmed) {
url_redirect('forum-post', [ url_redirect('forum-post', [
'post' => $postInfo['post_id'], 'post' => $postInfo->getId(),
'post_fragment' => 'p' . $postInfo['post_id'], 'post_fragment' => 'p' . $postInfo->getId(),
]); ]);
break; break;
} elseif(!$postRequestVerified) { } elseif(!$postRequestVerified) {
Template::render('forum.confirm', [ Template::render('forum.confirm', [
'title' => 'Confirm post deletion', 'title' => 'Confirm post deletion',
'class' => 'far fa-trash-alt', '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' => [ 'params' => [
'p' => $postInfo['post_id'], 'p' => $postInfo->getId(),
'm' => 'delete', 'm' => 'delete',
], ],
]); ]);
@ -139,16 +116,13 @@ switch($postMode) {
} }
} }
$deletePost = forum_post_delete($postInfo['post_id']); $postInfo->delete();
AuditLog::create(AuditLog::FORUM_POST_DELETE, [$postInfo->getId()]);
if($deletePost) {
AuditLog::create(AuditLog::FORUM_POST_DELETE, [$postInfo['post_id']]);
}
if($isXHR) { if($isXHR) {
echo json_encode([ echo json_encode([
'success' => $deletePost, 'success' => $deletePost,
'post_id' => $postInfo['post_id'], 'post_id' => $postInfo->getId(),
'message' => $deletePost ? 'Post deleted!' : 'Failed to delete post.', 'message' => $deletePost ? 'Post deleted!' : 'Failed to delete post.',
]); ]);
break; break;
@ -159,7 +133,7 @@ switch($postMode) {
break; break;
} }
url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]); url_redirect('forum-topic', ['topic' => $postInfo->getTopicId()]);
break; break;
case 'nuke': case 'nuke':
@ -171,17 +145,17 @@ switch($postMode) {
if(!$isXHR) { if(!$isXHR) {
if($postRequestVerified && !$submissionConfirmed) { if($postRequestVerified && !$submissionConfirmed) {
url_redirect('forum-post', [ url_redirect('forum-post', [
'post' => $postInfo['post_id'], 'post' => $postInfo->getId(),
'post_fragment' => 'p' . $postInfo['post_id'], 'post_fragment' => 'p' . $postInfo->getId(),
]); ]);
break; break;
} elseif(!$postRequestVerified) { } elseif(!$postRequestVerified) {
Template::render('forum.confirm', [ Template::render('forum.confirm', [
'title' => 'Confirm post nuke', 'title' => 'Confirm post nuke',
'class' => 'fas fa-radiation', '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' => [ 'params' => [
'p' => $postInfo['post_id'], 'p' => $postInfo->getId(),
'm' => 'nuke', 'm' => 'nuke',
], ],
]); ]);
@ -189,18 +163,12 @@ switch($postMode) {
} }
} }
$nukePost = forum_post_nuke($postInfo['post_id']); $postInfo->nuke();
AuditLog::create(AuditLog::FORUM_POST_NUKE, [$postInfo->getId()]);
if(!$nukePost) {
echo render_error(500);
break;
}
AuditLog::create(AuditLog::FORUM_POST_NUKE, [$postInfo['post_id']]);
http_response_code(204); http_response_code(204);
if(!$isXHR) { if(!$isXHR) {
url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]); url_redirect('forum-topic', ['topic' => $postInfo->getTopicId()]);
} }
break; break;
@ -213,17 +181,17 @@ switch($postMode) {
if(!$isXHR) { if(!$isXHR) {
if($postRequestVerified && !$submissionConfirmed) { if($postRequestVerified && !$submissionConfirmed) {
url_redirect('forum-post', [ url_redirect('forum-post', [
'post' => $postInfo['post_id'], 'post' => $postInfo->getId(),
'post_fragment' => 'p' . $postInfo['post_id'], 'post_fragment' => 'p' . $postInfo->getId(),
]); ]);
break; break;
} elseif(!$postRequestVerified) { } elseif(!$postRequestVerified) {
Template::render('forum.confirm', [ Template::render('forum.confirm', [
'title' => 'Confirm post restore', 'title' => 'Confirm post restore',
'class' => 'fas fa-magic', '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' => [ 'params' => [
'p' => $postInfo['post_id'], 'p' => $postInfo->getId(),
'm' => 'restore', 'm' => 'restore',
], ],
]); ]);
@ -231,49 +199,12 @@ switch($postMode) {
} }
} }
$restorePost = forum_post_restore($postInfo['post_id']); $postInfo->restore();
AuditLog::create(AuditLog::FORUM_POST_RESTORE, [$postInfo->getId()]);
if(!$restorePost) {
echo render_error(500);
break;
}
AuditLog::create(AuditLog::FORUM_POST_RESTORE, [$postInfo['post_id']]);
http_response_code(204); http_response_code(204);
if(!$isXHR) { if(!$isXHR) {
url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]); url_redirect('forum-topic', ['topic' => $postInfo->getTopicId()]);
} }
break; break;
default: // function as an alt for topic.php?p= by default
$canDeleteAny = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST);
if(!empty($postInfo['post_deleted']) && !$canDeleteAny) {
echo render_error(404);
break;
}
$postFind = forum_post_find($postInfo['post_id'], $currentUserId);
if(empty($postFind)) {
echo render_error(404);
break;
}
if($canDeleteAny) {
$postInfo['preceeding_post_count'] += $postInfo['preceeding_post_deleted_count'];
}
unset($postInfo['preceeding_post_deleted_count']);
if($isXHR) {
echo json_encode($postFind);
break;
}
url_redirect('forum-topic', [
'topic' => $postFind['topic_id'],
'page' => floor($postFind['preceeding_post_count'] / \Misuzu\Forum\ForumPost::PER_PAGE) + 1,
]);
} }

View file

@ -8,6 +8,8 @@ use Misuzu\Forum\ForumTopicNotFoundException;
use Misuzu\Forum\ForumTopicCreationFailedException; use Misuzu\Forum\ForumTopicCreationFailedException;
use Misuzu\Forum\ForumTopicUpdateFailedException; use Misuzu\Forum\ForumTopicUpdateFailedException;
use Misuzu\Forum\ForumPost; use Misuzu\Forum\ForumPost;
use Misuzu\Forum\ForumPostCreationFailedException;
use Misuzu\Forum\ForumPostUpdateFailedException;
use Misuzu\Forum\ForumPostNotFoundException; use Misuzu\Forum\ForumPostNotFoundException;
use Misuzu\Net\IPAddress; use Misuzu\Net\IPAddress;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
@ -71,13 +73,11 @@ if(empty($postId) && empty($topicId) && empty($forumId)) {
return; return;
} }
if(!empty($postId)) { if(!empty($postId))
$post = forum_post_get($postId); try {
$postInfo = ForumPost::byId($postId);
if(isset($post['topic_id'])) { // should automatic cross-quoting be a thing? if so, check if $topicId is < 1 first $topicId = $postInfo->getTopicId();
$topicId = (int)$post['topic_id']; } catch(ForumPostNotFoundException $ex) {}
}
}
if(!empty($topicId)) if(!empty($topicId))
try { try {
@ -122,12 +122,12 @@ if($mode === 'create' || $mode === 'edit') {
// edit mode stuff // edit mode stuff
if($mode === 'edit') { if($mode === 'edit') {
if(empty($post)) { if(empty($postInfo)) {
echo render_error(404); echo render_error(404);
return; return;
} }
if(!perms_check($perms, $post['poster_id'] === $currentUserId ? MSZ_FORUM_PERM_EDIT_POST : MSZ_FORUM_PERM_EDIT_ANY_POST)) { if(!perms_check($perms, $postInfo->getUserId() === $currentUserId ? MSZ_FORUM_PERM_EDIT_POST : MSZ_FORUM_PERM_EDIT_ANY_POST)) {
echo render_error(403); echo render_error(403);
return; return;
} }
@ -140,16 +140,16 @@ if(!empty($_POST)) {
$topicTitle = $_POST['post']['title'] ?? ''; $topicTitle = $_POST['post']['title'] ?? '';
$postText = $_POST['post']['text'] ?? ''; $postText = $_POST['post']['text'] ?? '';
$postParser = (int)($_POST['post']['parser'] ?? Parser::BBCODE); $postParser = (int)($_POST['post']['parser'] ?? Parser::BBCODE);
$topicType = isset($_POST['post']['type']) ? (int)$_POST['post']['type'] : null; $topicType = isset($_POST['post']['type']) ? (int)$_POST['post']['type'] : ForumTopic::TYPE_DISCUSSION;
$postSignature = isset($_POST['post']['signature']); $postSignature = isset($_POST['post']['signature']);
if(!CSRF::validateRequest()) { if(!CSRF::validateRequest()) {
$notices[] = 'Could not verify request.'; $notices[] = 'Could not verify request.';
} else { } else {
$isEditingTopic = $isNewTopic || ($mode === 'edit' && $post['is_opening_post']); $isEditingTopic = $isNewTopic || ($mode === 'edit' && $postInfo->isOpeningPost());
if($mode === 'create') { if($mode === 'create') {
$timeoutCheck = max(1, forum_timeout($forumInfo->getId(), $currentUserId)); $timeoutCheck = max(1, $forumInfo->checkCooldown($currentUser));
if($timeoutCheck < 5) { if($timeoutCheck < 5) {
$notices[] = sprintf("You're posting too quickly! Please wait %s seconds before posting again.", number_format($timeoutCheck)); $notices[] = sprintf("You're posting too quickly! Please wait %s seconds before posting again.", number_format($timeoutCheck));
@ -192,21 +192,25 @@ if(!empty($_POST)) {
$topicId = $topicInfo->getId(); $topicId = $topicInfo->getId();
} }
$postId = forum_post_create( $postInfo = ForumPost::create($topicInfo, $currentUser, IPAddress::remote(), $postText, $postParser, $postSignature);
$topicId, $postId = $postInfo->getId();
$forumInfo->getId(),
$currentUserId, $topicInfo->markRead($currentUser);
IPAddress::remote(),
$postText,
$postParser,
$postSignature
);
forum_topic_mark_read($currentUserId, $topicId, $forumInfo->getId());
$forumInfo->increaseTopicPostCount($isNewTopic); $forumInfo->increaseTopicPostCount($isNewTopic);
break; break;
case 'edit': case 'edit':
if(!forum_post_update($postId, IPAddress::remote(), $postText, $postParser, $postSignature, $postText !== $post['post_text'])) { if($postText !== $postInfo->getBody() && $postInfo->shouldBumpEdited())
$postInfo->bumpEdited();
$postInfo->setRemoteAddress(IPAddress::remote())
->setBody($postText)
->setBodyParser($postParser)
->setDisplaySignature($postSignature);
try {
$postInfo->update();
} catch(ForumPostUpdateFailedException $ex) {
$notices[] = 'Post edit failed.'; $notices[] = 'Post edit failed.';
} }
@ -240,7 +244,7 @@ if(!$isNewTopic && !empty($topicInfo)) {
} }
if($mode === 'edit') { // $post is pretty much sure to be populated at this point if($mode === 'edit') { // $post is pretty much sure to be populated at this point
Template::set('posting_post', $post); Template::set('posting_post', $postInfo);
} }
Template::render('forum.posting', [ Template::render('forum.posting', [

View file

@ -19,12 +19,14 @@ $submissionConfirmed = filter_input(INPUT_GET, 'confirm') === '1';
$topicUser = User::getCurrent(); $topicUser = User::getCurrent();
$topicUserId = $topicUser === null ? 0 : $topicUser->getId(); $topicUserId = $topicUser === null ? 0 : $topicUser->getId();
if($topicId < 1 && $postId > 0) { if($topicId < 1 && $postId > 0)
$postInfo = forum_post_find($postId, $topicUserId); try {
$postInfo = ForumPost::byId($postId);
if(!empty($postInfo['topic_id'])) $topicId = $postInfo->getTopicId();
$topicId = (int)$postInfo['topic_id']; } catch(ForumPostNotFoundException $ex) {
} echo render_error(404);
return;
}
try { try {
$topicInfo = ForumTopic::byId($topicId); $topicInfo = ForumTopic::byId($topicId);
@ -113,8 +115,8 @@ if(in_array($moderationMode, $validModerationModes, true)) {
'posts' => 403, 'posts' => 403,
'' => 200, '' => 200,
]; ];
$canDelete = $topicInfo->canDelete($topicUser); $canDelete = $topicInfo->canBeDeleted($topicUser);
$canDeleteMsg = ForumTopic::canDeleteErrorString($canDelete); $canDeleteMsg = ForumTopic::canBeDeletedErrorString($canDelete);
$responseCode = $canDeleteCodes[$canDelete] ?? 500; $responseCode = $canDeleteCodes[$canDelete] ?? 500;
if($canDelete !== '') { if($canDelete !== '') {
@ -283,15 +285,8 @@ if(in_array($moderationMode, $validModerationModes, true)) {
$topicPagination = new Pagination($topicInfo->getActualPostCount($canDeleteAny), \Misuzu\Forum\ForumPost::PER_PAGE, 'page'); $topicPagination = new Pagination($topicInfo->getActualPostCount($canDeleteAny), \Misuzu\Forum\ForumPost::PER_PAGE, 'page');
if(isset($postInfo['preceeding_post_count'])) { if(isset($postInfo))
$preceedingPosts = $postInfo['preceeding_post_count']; $topicPagination->setPage($postInfo->getTopicPage($canDeleteAny, $topicPagination->getRange()));
if($canDeleteAny) {
$preceedingPosts += $postInfo['preceeding_post_deleted_count'];
}
$topicPagination->setPage(floor($preceedingPosts / $topicPagination->getRange()), true);
}
if(!$topicPagination->hasValidOffset()) { if(!$topicPagination->hasValidOffset()) {
echo render_error(404); echo render_error(404);
@ -300,7 +295,7 @@ if(!$topicPagination->hasValidOffset()) {
$canReply = !$topicInfo->isArchived() && !$topicInfo->isLocked() && !$topicInfo->isDeleted() && perms_check($perms, MSZ_FORUM_PERM_CREATE_POST); $canReply = !$topicInfo->isArchived() && !$topicInfo->isLocked() && !$topicInfo->isDeleted() && perms_check($perms, MSZ_FORUM_PERM_CREATE_POST);
forum_topic_mark_read($topicUserId, $topicInfo->getId(), $topicInfo->getCategoryId()); $topicInfo->markRead($topicUser);
Template::render('forum.topic', [ Template::render('forum.topic', [
'topic_perms' => $perms, 'topic_perms' => $perms,

View file

@ -2,6 +2,7 @@
namespace Misuzu; namespace Misuzu;
use Misuzu\Forum\ForumTopic; use Misuzu\Forum\ForumTopic;
use Misuzu\Forum\ForumPost;
use Misuzu\News\NewsPost; use Misuzu\News\NewsPost;
use Misuzu\Users\User; use Misuzu\Users\User;
@ -11,7 +12,7 @@ $searchQuery = !empty($_GET['q']) && is_string($_GET['q']) ? $_GET['q'] : '';
if(!empty($searchQuery)) { if(!empty($searchQuery)) {
$forumTopics = ForumTopic::bySearchQuery($searchQuery); $forumTopics = ForumTopic::bySearchQuery($searchQuery);
$forumPosts = forum_post_search($searchQuery); $forumPosts = ForumPost::bySearchQuery($searchQuery);
$newsPosts = NewsPost::bySearchQuery($searchQuery); $newsPosts = NewsPost::bySearchQuery($searchQuery);
$findUsers = DB::prepare(sprintf( $findUsers = DB::prepare(sprintf(

View file

@ -28,11 +28,19 @@ class CronCommand implements CommandInterface {
case 'func': case 'func':
call_user_func($task['command']); call_user_func($task['command']);
break; break;
case 'selffunc':
call_user_func(self::class . '::' . $task['command']);
break;
} }
} }
} }
} }
private static function syncForum(): void {
\Misuzu\Forum\ForumCategory::root()->synchronise(true);
}
private const TASKS = [ private const TASKS = [
[ [
'name' => 'Ensures main role exists.', 'name' => 'Ensures main role exists.',
@ -147,9 +155,9 @@ class CronCommand implements CommandInterface {
], ],
[ [
'name' => 'Recount forum topics and posts.', 'name' => 'Recount forum topics and posts.',
'type' => 'func', 'type' => 'selffunc',
'slow' => true, 'slow' => true,
'command' => 'forum_count_synchronise', 'command' => 'syncForum',
], ],
[ [
'name' => 'Clean up expired tfa tokens.', 'name' => 'Clean up expired tfa tokens.',

View file

@ -351,15 +351,77 @@ class ForumCategory {
return $this->checkLegacyPermission($user, MSZ_FORUM_PERM_SET_READ); return $this->checkLegacyPermission($user, MSZ_FORUM_PERM_SET_READ);
} }
public function hasUnread(?User $user): bool { public function hasRead(User $user): bool {
if($user === null) static $cache = [];
return false;
return forum_topics_unread($this->getId(), $user->getId()); $cacheId = $user->getId() . ':' . $this->getId();
if(isset($cache[$cacheId]))
return $cache[$cacheId];
if(!$this->canView($user))
return $cache[$cacheId] = true;
$countUnread = (int)DB::prepare(
'SELECT COUNT(*) FROM `' . DB::PREFIX . ForumTopic::TABLE . '` AS ti'
. ' LEFT JOIN `' . DB::PREFIX . ForumTopicTrack::TABLE . '` AS tt'
. ' ON tt.`topic_id` = ti.`topic_id` AND tt.`user_id` = :user'
. ' WHERE ti.`forum_id` = :forum AND ti.`topic_deleted` IS NULL'
. ' AND ti.`topic_bumped` >= NOW() - INTERVAL :limit SECOND'
. ' AND (tt.`track_last_read` IS NULL OR tt.`track_last_read` < ti.`topic_bumped`)'
)->bind('forum', $this->getId())
->bind('user', $user->getId())
->bind('limit', ForumTopic::UNREAD_TIME_LIMIT)
->fetchColumn();
if($countUnread > 0)
return $cache[$cacheId] = false;
foreach($this->getChildren() as $child)
if(!$child->hasRead($user))
return $cache[$cacheId] = false;
return $cache[$cacheId] = true;
} }
public function markAsRead(User $user, bool $recursive = true): void { public function markAsRead(User $user, bool $recursive = true): void {
// Recursion is implied for now if($this->isRoot()) {
// Also forego recursion if we're root and just mark the entire forum as expected if(!$recursive)
forum_mark_read($this->isRoot() ? null : $this->getId(), $user->getId()); return;
$recursive = false;
}
if($recursive) {
$children = $this->getChildren($user);
foreach($children as $child)
$child->markAsRead($user, true);
}
$mark = DB::prepare(
'INSERT INTO `' . DB::PREFIX . ForumTopicTrack::TABLE . '`'
. ' (`user_id`, `topic_id`, `forum_id`, `track_last_read`)'
. ' SELECT u.`user_id`, t.`topic_id`, t.`forum_id`, NOW()'
. ' FROM `msz_forum_topics` AS t'
. ' LEFT JOIN `msz_users` AS u ON u.`user_id` = :user'
. ' WHERE t.`topic_deleted` IS NULL'
. ' AND t.`topic_bumped` >= NOW() - INTERVAL :limit SECOND'
. ($this->isRoot() ? '' : ' AND t.`forum_id` = :forum')
. ' GROUP BY t.`topic_id`'
. ' ON DUPLICATE KEY UPDATE `track_last_read` = NOW()'
)->bind('user', $user->getId())
->bind('limit', ForumTopic::UNREAD_TIME_LIMIT);
if(!$this->isRoot())
$mark->bind('forum', $this->getId());
$mark->execute();
}
public function checkCooldown(User $user): int {
return (int)DB::prepare(
'SELECT TIMESTAMPDIFF(SECOND, COALESCE(MAX(`post_created`), NOW() - INTERVAL 1 YEAR), NOW())'
. ' FROM `' . DB::PREFIX . ForumPost::TABLE . '`'
. ' WHERE `forum_id` = :forum AND `user_id` = :user'
)->bind('forum', $this->getId())->bind('user', $user->getId())->fetchColumn();
} }
public function getLatestTopic(?User $viewer = null): ?ForumTopic { public function getLatestTopic(?User $viewer = null): ?ForumTopic {

View file

@ -11,6 +11,7 @@ use Misuzu\Users\UserNotFoundException;
class ForumPostException extends ForumException {} class ForumPostException extends ForumException {}
class ForumPostNotFoundException extends ForumPostException {} class ForumPostNotFoundException extends ForumPostException {}
class ForumPostCreationFailedException extends ForumPostException {} class ForumPostCreationFailedException extends ForumPostException {}
class ForumPostUpdateFailedException extends ForumPostException {}
class ForumPost { class ForumPost {
public const PER_PAGE = 10; public const PER_PAGE = 10;
@ -18,6 +19,10 @@ class ForumPost {
public const BODY_MIN_LENGTH = 1; public const BODY_MIN_LENGTH = 1;
public const BODY_MAX_LENGTH = 60000; public const BODY_MAX_LENGTH = 60000;
public const EDIT_BUMP_THRESHOLD = 60 * 5;
public const DELETE_AGE_LIMIT = 60 * 60 * 24 * 7;
// Database fields // Database fields
private $post_id = -1; private $post_id = -1;
private $topic_id = -1; private $topic_id = -1;
@ -117,6 +122,10 @@ class ForumPost {
public function getRemoteAddress(): string { public function getRemoteAddress(): string {
return $this->post_ip; return $this->post_ip;
} }
public function setRemoteAddress(string $remoteAddress): self {
$this->post_ip = $remoteAddress;
return $this;
}
public function getBody(): string { public function getBody(): string {
return $this->post_text; return $this->post_text;
@ -157,6 +166,12 @@ class ForumPost {
public function getCreatedTime(): int { public function getCreatedTime(): int {
return $this->post_created === null ? -1 : $this->post_created; return $this->post_created === null ? -1 : $this->post_created;
} }
public function getAge(): int {
return time() - $this->getCreatedTime();
}
public function shouldBumpEdited(): bool {
return $this->getAge() > self::EDIT_BUMP_THRESHOLD;
}
public function getEditedTime(): int { public function getEditedTime(): int {
return $this->post_edited === null ? -1 : $this->post_edited; return $this->post_edited === null ? -1 : $this->post_edited;
@ -188,6 +203,17 @@ class ForumPost {
return $this->getTopic()->isTopicAuthor($this->getUser()); return $this->getTopic()->isTopicAuthor($this->getUser());
} }
public function getTopicOffset(bool $includeDeleted = false): int {
return (int)DB::prepare(
'SELECT COUNT(`post_id`) FROM `' . DB::PREFIX . self::TABLE . '`'
. ' WHERE `topic_id` = :topic AND `post_id` < :post'
. ($includeDeleted ? '' : ' AND `post_deleted` IS NULL')
)->bind('topic', $this->getTopicId())->bind('post', $this->getId())->fetchColumn();
}
public function getTopicPage(bool $includeDeleted = false, int $postsPerPage = self::PER_PAGE): int {
return floor($this->getTopicOffset() / $postsPerPage) + 1;
}
public function canBeSeen(?User $user): bool { public function canBeSeen(?User $user): bool {
if($user === null && $this->isDeleted()) if($user === null && $this->isDeleted())
return false; return false;
@ -202,13 +228,6 @@ class ForumPost {
return $this->getUser()->getId() === $user->getId(); return $this->getUser()->getId() === $user->getId();
} }
// complete this implementation
public function canBeDeleted(?User $user): bool {
if($user === null)
return false;
return $this->getUser()->getId() === $user->getId();
}
public static function validateBody(string $body): string { public static function validateBody(string $body): string {
$length = mb_strlen(trim($body)); $length = mb_strlen(trim($body));
if($length < self::BODY_MIN_LENGTH) if($length < self::BODY_MIN_LENGTH)
@ -230,6 +249,73 @@ class ForumPost {
} }
} }
public function canBeDeleted(User $user): string {
if(false) // check if viewable
return 'view';
if($this->isOpeningPost())
return 'opening';
// check if user can view deleted posts/is mod
$canDeleteAny = false;
if($this->isDeleted())
return $canDeleteAny ? 'deleted' : 'view';
if(!$canDeleteAny) {
if(false) // check if user can delete posts
return 'permission';
if($user->getId() !== $this->getUserId())
return 'owner';
if($this->getCreatedTime() <= time() - self::DELETE_AGE_LIMIT)
return 'age';
}
return '';
}
public static function canBeDeletedErrorString(string $error): string {
switch($error) {
case 'view':
return 'This post doesn\'t exist.';
case 'deleted':
return 'This post has already been marked as deleted.';
case 'permission':
return 'You aren\'t allowed to this post.';
case 'owner':
return 'You can only delete your own posts.';
case 'age':
return 'This post is too old to be deleted. Ask a moderator to remove it if you deem it absolutely necessary.';
case '':
return 'Post can be deleted!';
default:
return 'Post cannot be deleted.';
}
}
public function delete(): void {
if($this->isDeleted())
return;
$this->post_deleted = time();
DB::prepare('UPDATE `' . DB::PREFIX . self::TABLE . '` SET `post_deleted` = NOW() WHERE `post_id` = :post')
->bind('post', $this->getId())
->execute();
}
public function restore(): void {
if(!$this->isDeleted())
return;
$this->post_deleted = null;
DB::prepare('UPDATE `' . DB::PREFIX . self::TABLE . '` SET `post_deleted` = NULL WHERE `post_id` = :post')
->bind('post', $this->getId())
->execute();
}
public function nuke(): void {
if(!$this->isDeleted())
return;
DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `post_id` = :post')
->bind('post', $this->getId())
->execute();
}
public static function deleteTopic(ForumTopic $topic): void { public static function deleteTopic(ForumTopic $topic): void {
// Deleting posts should only be possible while the topic is already in a deleted state // Deleting posts should only be possible while the topic is already in a deleted state
if(!$topic->isDeleted()) if(!$topic->isDeleted())
@ -261,6 +347,62 @@ class ForumPost {
->execute(); ->execute();
} }
public static function create(
ForumTopic $topic,
User $user,
string $ipAddress,
string $text,
int $parser = Parser::PLAIN,
bool $displaySignature = true
): ForumPost {
$create = DB::prepare(
'INSERT INTO `msz_forum_posts` ('
. '`topic_id`, `forum_id`, `user_id`, `post_ip`, `post_text`, `post_parse`, `post_display_signature`'
. ') VALUES (:topic, :forum, :user, INET6_ATON(:ip), :body, :parser, :display_signature)'
)->bind('topic', $topic->getId())
->bind('forum', $topic->getCategoryId())
->bind('user', $user->getId())
->bind('ip', $ipAddress)
->bind('body', $text)
->bind('parser', $parser)
->bind('display_signature', $displaySignature ? 1 : 0)
->execute();
if(!$create)
throw new ForumPostCreationFailedException;
$postId = DB::lastId();
if($postId < 1)
throw new ForumPostCreationFailedException;
try {
return self::byId($postId);
} catch(ForumPostNotFoundException $ex) {
throw new ForumPostCreationFailedException;
}
}
public function update(): void {
if($this->getId() < 1)
throw new ForumPostUpdateFailedException;
if(!DB::prepare(
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
. ' SET `post_ip` = INET6_ATON(:ip),'
. ' `post_text` = :body,'
. ' `post_parse` = :parser,'
. ' `post_display_signature` = :display_signature,'
. ' `post_edited` = FROM_UNIXTIME(:edited)'
. ' WHERE `post_id` = :post'
)->bind('post', $this->getId())
->bind('ip', $this->getRemoteAddress())
->bind('body', $this->getBody())
->bind('parser', $this->getBodyParser())
->bind('display_signature', $this->shouldDisplaySignature() ? 1 : 0)
->bind('edited', $this->isEdited() ? $this->getEditedTime() : null)
->execute())
throw new ForumPostUpdateFailedException;
}
private static function countQueryBase(): string { private static function countQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf('COUNT(*)', self::TABLE)); return sprintf(self::QUERY_SELECT, sprintf('COUNT(*)', self::TABLE));
} }
@ -321,4 +463,26 @@ class ForumPost {
$memoizer->insert($objects[] = $object); $memoizer->insert($objects[] = $object);
return $objects; return $objects;
} }
public static function bySearchQuery(string $search, bool $includeDeleted = false, ?Pagination $pagination = null): array {
$query = self::byQueryBase()
. ' WHERE MATCH(`post_text`) AGAINST (:search IN NATURAL LANGUAGE MODE)'
. ($includeDeleted ? '' : ' AND `post_deleted` IS NULL')
. ' ORDER BY `post_id`';
if($pagination !== null)
$query .= ' LIMIT :range OFFSET :offset';
$getObjects = DB::prepare($query)
->bind('search', $search);
if($pagination !== null)
$getObjects->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
$objects = [];
$memoizer = self::memoizer();
while($object = $getObjects->fetchObject(self::class))
$memoizer->insert($objects[] = $object);
return $objects;
}
} }

View file

@ -43,6 +43,8 @@ class ForumTopic {
public const DELETE_AGE_LIMIT = 60 * 60 * 24; public const DELETE_AGE_LIMIT = 60 * 60 * 24;
public const DELETE_POST_LIMIT = 2; public const DELETE_POST_LIMIT = 2;
public const UNREAD_TIME_LIMIT = 60 * 60 * 24 * 31;
// Database fields // Database fields
private $topic_id = -1; private $topic_id = -1;
private $forum_id = -1; private $forum_id = -1;
@ -155,7 +157,7 @@ class ForumTopic {
if($this->hasPriorityVoting()) if($this->hasPriorityVoting())
return 'far fa-star fa-fw'; return 'far fa-star fa-fw';
return ($this->hasUnread($viewer) ? 'fas' : 'far') . ' fa-comment fa-fw'; return ($viewer === null || $this->hasRead($viewer) ? 'far' : 'fas') . ' fa-comment fa-fw';
} }
public function getTitle(): string { public function getTitle(): string {
@ -180,6 +182,12 @@ class ForumTopic {
public function getViewCount(): int { public function getViewCount(): int {
return $this->topic_count_views; return $this->topic_count_views;
} }
public function incrementViewCount(): void {
++$this->topic_count_views;
DB::prepare('UPDATE `msz_forum_topics` SET `topic_count_views` = `topic_count_views` + 1 WHERE `topic_id` = :topic')
->bind('topic', $this->getId())
->execute();
}
public function getFirstPostId(): int { public function getFirstPostId(): int {
return $this->topic_post_first < 1 ? -1 : $this->topic_post_first; return $this->topic_post_first < 1 ? -1 : $this->topic_post_first;
@ -274,10 +282,24 @@ class ForumTopic {
return $this->polls; return $this->polls;
} }
public function hasUnread(?User $user): bool { public function isAbandoned(): bool {
if($user === null) return $this->getBumpedTime() < time() - self::UNREAD_TIME_LIMIT;
}
public function hasRead(User $user): bool {
if($this->isAbandoned())
return true;
try {
$trackInfo = ForumTopicTrack::byTopicAndUser($this, $user);
return $trackInfo->getReadTime() >= $this->getBumpedTime();
} catch(ForumTopicTrackNotFoundException $ex) {
return false; return false;
return mt_rand(0, 10) >= 5; }
}
public function markRead(User $user): void {
if(!$this->hasRead($user))
$this->incrementViewCount();
ForumTopicTrack::bump($this, $user);
} }
public function hasParticipated(?User $user): bool { public function hasParticipated(?User $user): bool {
@ -307,7 +329,7 @@ class ForumTopic {
return $this->getCategory()->canView($user); return $this->getCategory()->canView($user);
} }
public function canDelete(User $user): string { public function canBeDeleted(User $user): string {
if(false) // check if viewable if(false) // check if viewable
return 'view'; return 'view';
@ -330,7 +352,7 @@ class ForumTopic {
return ''; return '';
} }
public static function canDeleteErrorString(string $error): string { public static function canBeDeletedErrorString(string $error): string {
switch($error) { switch($error) {
case 'view': case 'view':
return 'This topic doesn\'t exist.'; return 'This topic doesn\'t exist.';
@ -402,7 +424,7 @@ class ForumTopic {
throw new ForumTopicUpdateFailedException; throw new ForumTopicUpdateFailedException;
if(!DB::prepare( if(!DB::prepare(
'UPDATE `msz_forum_topics`' 'UPDATE `' . DB::PREFIX . self::TABLE . '`'
. ' SET `topic_title` = :title,' . ' SET `topic_title` = :title,'
. ' `topic_type` = :type' . ' `topic_type` = :type'
. ' WHERE `topic_id` = :topic' . ' WHERE `topic_id` = :topic'

View file

@ -2,8 +2,12 @@
namespace Misuzu\Forum; namespace Misuzu\Forum;
use Misuzu\DB; use Misuzu\DB;
use Misuzu\Memoizer;
use Misuzu\Users\User; use Misuzu\Users\User;
class ForumTopicTrackException extends ForumException {}
class ForumTopicTrackNotFoundException extends ForumTopicTrackException {}
class ForumTopicTrack { class ForumTopicTrack {
// Database fields // Database fields
private $user_id = -1; private $user_id = -1;
@ -50,4 +54,52 @@ class ForumTopicTrack {
public function getReadTime(): int { public function getReadTime(): int {
return $this->track_last_read === null ? -1 : $this->track_last_read; return $this->track_last_read === null ? -1 : $this->track_last_read;
} }
public static function bump(ForumTopic $topic, User $user): void {
DB::prepare(
'REPLACE INTO `' . DB::PREFIX . self::TABLE . '`'
. ' (`user_id`, `topic_id`, `forum_id`, `track_last_read`)'
. ' VALUES (:user, :topic, :forum, NOW())'
)->bind('user', $user->getId())
->bind('topic', $topic->getId())
->bind('forum', $topic->getCategoryId())
->execute();
}
private static function memoizer() {
static $memoizer = null;
if($memoizer === null)
$memoizer = new Memoizer;
return $memoizer;
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byTopicAndUser(ForumTopic $topic, User $user): ForumTopicTrack {
return self::memoizer()->find(function($track) use ($topic, $user) {
return $track->getTopicId() === $topic->getId() && $track->getUserId() === $user->getId();
}, function() use ($topic, $user) {
$obj = DB::prepare(self::byQueryBase() . ' WHERE `topic_id` = :topic AND `user_id` = :user')
->bind('topic', $topic->getId())
->bind('user', $user->getId())
->fetchObject(self::class);
if(!$obj)
throw new ForumTopicTrackNotFoundException;
return $obj;
});
}
public static function byCategoryAndUser(ForumCategory $category, User $user): ForumTopicTrack {
return self::memoizer()->find(function($track) use ($category, $user) {
return $track->getCategoryId() === $category->getId() && $track->getUserId() === $user->getId();
}, function() use ($category, $user) {
$obj = DB::prepare(self::byQueryBase() . ' WHERE `forum_id` = :category AND `user_id` = :user')
->bind('category', $category->getId())
->bind('user', $user->getId())
->fetchObject(self::class);
if(!$obj)
throw new ForumTopicTrackNotFoundException;
return $obj;
});
}
} }

View file

@ -1,147 +0,0 @@
<?php
function forum_get_parent_id(int $forumId): int {
if($forumId < 1) {
return 0;
}
static $memoized = [];
if(array_key_exists($forumId, $memoized)) {
return $memoized[$forumId];
}
$getParent = \Misuzu\DB::prepare('
SELECT `forum_parent`
FROM `msz_forum_categories`
WHERE `forum_id` = :forum_id
');
$getParent->bind('forum_id', $forumId);
return (int)$getParent->fetchColumn();
}
function forum_get_child_ids(int $forumId): array {
if($forumId < 1) {
return [];
}
static $memoized = [];
if(array_key_exists($forumId, $memoized)) {
return $memoized[$forumId];
}
$getChildren = \Misuzu\DB::prepare('
SELECT `forum_id`
FROM `msz_forum_categories`
WHERE `forum_parent` = :forum_id
');
$getChildren->bind('forum_id', $forumId);
$children = $getChildren->fetchAll();
return $memoized[$forumId] = array_column($children, 'forum_id');
}
function forum_topics_unread(int $forumId, int $userId): int {
if($userId < 1 || $forumId < 1) {
return false;
}
static $memoized = [];
$memoId = "{$forumId}-{$userId}";
if(array_key_exists($memoId, $memoized)) {
return $memoized[$memoId];
}
$memoized[$memoId] = 0;
$children = forum_get_child_ids($forumId);
foreach($children as $child) {
$memoized[$memoId] += forum_topics_unread($child, $userId);
}
if(forum_perms_check_user(MSZ_FORUM_PERMS_GENERAL, $forumId, $userId, MSZ_FORUM_PERM_SET_READ)) {
$countUnread = \Misuzu\DB::prepare('
SELECT COUNT(ti.`topic_id`)
FROM `msz_forum_topics` AS ti
LEFT JOIN `msz_forum_topics_track` AS tt
ON tt.`topic_id` = ti.`topic_id` AND tt.`user_id` = :user_id
WHERE ti.`forum_id` = :forum_id
AND ti.`topic_deleted` IS NULL
AND ti.`topic_bumped` >= NOW() - INTERVAL 1 MONTH
AND (
tt.`track_last_read` IS NULL
OR tt.`track_last_read` < ti.`topic_bumped`
)
');
$countUnread->bind('forum_id', $forumId);
$countUnread->bind('user_id', $userId);
$memoized[$memoId] += (int)$countUnread->fetchColumn();
}
return $memoized[$memoId];
}
function forum_timeout(int $forumId, int $userId): int {
$checkTimeout = \Misuzu\DB::prepare('
SELECT TIMESTAMPDIFF(SECOND, COALESCE(MAX(`post_created`), NOW() - INTERVAL 1 YEAR), NOW())
FROM `msz_forum_posts`
WHERE `forum_id` = :forum_id
AND `user_id` = :user_id
');
$checkTimeout->bind('forum_id', $forumId);
$checkTimeout->bind('user_id', $userId);
return (int)$checkTimeout->fetchColumn();
}
// $forumId == null marks all forums as read
function forum_mark_read(?int $forumId, int $userId): void {
if(($forumId !== null && $forumId < 1) || $userId < 1) {
return;
}
$entireForum = $forumId === null;
if(!$entireForum) {
$children = forum_get_child_ids($forumId);
foreach($children as $child) {
forum_mark_read($child, $userId);
}
}
$doMark = \Misuzu\DB::prepare(sprintf(
'
INSERT INTO `msz_forum_topics_track`
(`user_id`, `topic_id`, `forum_id`, `track_last_read`)
SELECT u.`user_id`, t.`topic_id`, t.`forum_id`, NOW()
FROM `msz_forum_topics` AS t
LEFT JOIN `msz_users` AS u
ON u.`user_id` = :user
WHERE t.`topic_deleted` IS NULL
AND t.`topic_bumped` >= NOW() - INTERVAL 1 MONTH
%1$s
GROUP BY t.`topic_id`
ON DUPLICATE KEY UPDATE
`track_last_read` = NOW()
',
$entireForum ? '' : 'AND t.`forum_id` = :forum'
));
$doMark->bind('user', $userId);
if(!$entireForum) {
$doMark->bind('forum', $forumId);
}
$doMark->execute();
}
function forum_count_synchronise(int $forumId = \Misuzu\Forum\ForumCategory::ROOT_ID, bool $save = true): array {
try {
return \Misuzu\Forum\ForumCategory::byId($forumId)->synchronise($save);
} catch(\Misuzu\Forum\ForumCategoryNotFoundException $ex) {
return ['topics' => 0, 'posts' => 0];
}
}

View file

@ -43,6 +43,27 @@ define('MSZ_FORUM_PERM_MODES', [
MSZ_FORUM_PERMS_GENERAL, MSZ_FORUM_PERMS_GENERAL,
]); ]);
function forum_get_parent_id(int $forumId): int {
if($forumId < 1) {
return 0;
}
static $memoized = [];
if(array_key_exists($forumId, $memoized)) {
return $memoized[$forumId];
}
$getParent = \Misuzu\DB::prepare('
SELECT `forum_parent`
FROM `msz_forum_categories`
WHERE `forum_id` = :forum_id
');
$getParent->bind('forum_id', $forumId);
return (int)$getParent->fetchColumn();
}
function forum_perms_get_user(?int $forum, int $user): array { function forum_perms_get_user(?int $forum, int $user): array {
$perms = perms_get_blank(MSZ_FORUM_PERM_MODES); $perms = perms_get_blank(MSZ_FORUM_PERM_MODES);

View file

@ -1,358 +0,0 @@
<?php
function forum_post_create(
int $topicId,
int $forumId,
int $userId,
string $ipAddress,
string $text,
int $parser = \Misuzu\Parsers\Parser::PLAIN,
bool $displaySignature = true
): int {
$createPost = \Misuzu\DB::prepare('
INSERT INTO `msz_forum_posts`
(`topic_id`, `forum_id`, `user_id`, `post_ip`, `post_text`, `post_parse`, `post_display_signature`)
VALUES
(:topic_id, :forum_id, :user_id, INET6_ATON(:post_ip), :post_text, :post_parse, :post_display_signature)
');
$createPost->bind('topic_id', $topicId);
$createPost->bind('forum_id', $forumId);
$createPost->bind('user_id', $userId);
$createPost->bind('post_ip', $ipAddress);
$createPost->bind('post_text', $text);
$createPost->bind('post_parse', $parser);
$createPost->bind('post_display_signature', $displaySignature ? 1 : 0);
return $createPost->execute() ? \Misuzu\DB::lastId() : 0;
}
function forum_post_update(
int $postId,
string $ipAddress,
string $text,
int $parser = \Misuzu\Parsers\Parser::PLAIN,
bool $displaySignature = true,
bool $bumpUpdate = true
): bool {
if($postId < 1) {
return false;
}
$updatePost = \Misuzu\DB::prepare('
UPDATE `msz_forum_posts`
SET `post_ip` = INET6_ATON(:post_ip),
`post_text` = :post_text,
`post_parse` = :post_parse,
`post_display_signature` = :post_display_signature,
`post_edited` = IF(:bump, NOW(), `post_edited`)
WHERE `post_id` = :post_id
');
$updatePost->bind('post_id', $postId);
$updatePost->bind('post_ip', $ipAddress);
$updatePost->bind('post_text', $text);
$updatePost->bind('post_parse', $parser);
$updatePost->bind('post_display_signature', $displaySignature ? 1 : 0);
$updatePost->bind('bump', $bumpUpdate ? 1 : 0);
return $updatePost->execute();
}
function forum_post_find(int $postId, int $userId): array {
$getPostInfo = \Misuzu\DB::prepare(sprintf(
'
SELECT
p.`post_id`, p.`topic_id`,
(
SELECT COUNT(`post_id`)
FROM `msz_forum_posts`
WHERE `topic_id` = p.`topic_id`
AND `post_id` < p.`post_id`
AND `post_deleted` IS NULL
ORDER BY `post_id`
) as `preceeding_post_count`,
(
SELECT COUNT(`post_id`)
FROM `msz_forum_posts`
WHERE `topic_id` = p.`topic_id`
AND `post_id` < p.`post_id`
AND `post_deleted` IS NOT NULL
ORDER BY `post_id`
) as `preceeding_post_deleted_count`
FROM `msz_forum_posts` AS p
WHERE p.`post_id` = :post_id
'));
$getPostInfo->bind('post_id', $postId);
return $getPostInfo->fetch();
}
function forum_post_get(int $postId, bool $allowDeleted = false): array {
$getPost = \Misuzu\DB::prepare(sprintf(
'
SELECT
p.`post_id`, p.`post_text`, p.`post_created`, p.`post_parse`, p.`post_display_signature`,
p.`topic_id`, p.`post_deleted`, p.`post_edited`, p.`topic_id`, p.`forum_id`,
INET6_NTOA(p.`post_ip`) AS `post_ip`,
u.`user_id` AS `poster_id`, u.`username` AS `poster_name`,
u.`user_created` AS `poster_joined`, u.`user_country` AS `poster_country`,
COALESCE(u.`user_colour`, r.`role_colour`) AS `poster_colour`,
(
SELECT COUNT(`post_id`)
FROM `msz_forum_posts`
WHERE `user_id` = p.`user_id`
AND `post_deleted` IS NULL
) AS `poster_post_count`,
(
SELECT MIN(`post_id`) = p.`post_id`
FROM `msz_forum_posts`
WHERE `topic_id` = p.`topic_id`
) AS `is_opening_post`,
(
SELECT `user_id` = u.`user_id`
FROM `msz_forum_posts`
WHERE `topic_id` = p.`topic_id`
ORDER BY `post_id`
LIMIT 1
) AS `is_original_poster`
FROM `msz_forum_posts` AS p
LEFT JOIN `msz_users` AS u
ON u.`user_id` = p.`user_id`
LEFT JOIN `msz_roles` AS r
ON r.`role_id` = u.`display_role`
WHERE `post_id` = :post_id
%1$s
ORDER BY `post_id`
',
$allowDeleted ? '' : 'AND `post_deleted` IS NULL'
));
$getPost->bind('post_id', $postId);
return $getPost->fetch();
}
function forum_post_search(string $query): array {
$searchPosts = \Misuzu\DB::prepare('
SELECT
p.`post_id`, p.`post_text`, p.`post_created`, p.`post_parse`, p.`post_display_signature`,
p.`topic_id`, p.`post_deleted`, p.`post_edited`, p.`topic_id`, p.`forum_id`,
INET6_NTOA(p.`post_ip`) AS `post_ip`,
u.`user_id` AS `poster_id`, u.`username` AS `poster_name`,
u.`user_created` AS `poster_joined`, u.`user_country` AS `poster_country`,
u.`user_signature_content` AS `poster_signature_content`, u.`user_signature_parser` AS `poster_signature_parser`,
COALESCE(u.`user_colour`, r.`role_colour`) AS `poster_colour`,
COALESCE(u.`user_title`, r.`role_title`) AS `poster_title`,
(
SELECT COUNT(`post_id`)
FROM `msz_forum_posts`
WHERE `user_id` = p.`user_id`
AND `post_deleted` IS NULL
) AS `poster_post_count`,
(
SELECT MIN(`post_id`) = p.`post_id`
FROM `msz_forum_posts`
WHERE `topic_id` = p.`topic_id`
) AS `is_opening_post`,
(
SELECT `user_id` = u.`user_id`
FROM `msz_forum_posts`
WHERE `topic_id` = p.`topic_id`
ORDER BY `post_id`
LIMIT 1
) AS `is_original_poster`
FROM `msz_forum_posts` AS p
LEFT JOIN `msz_users` AS u
ON u.`user_id` = p.`user_id`
LEFT JOIN `msz_roles` AS r
ON r.`role_id` = u.`display_role`
WHERE MATCH(p.`post_text`)
AGAINST (:query IN NATURAL LANGUAGE MODE)
AND `post_deleted` IS NULL
ORDER BY `post_id`
');
$searchPosts->bind('query', $query);
return $searchPosts->fetchAll();
}
function forum_post_count_user(int $userId, bool $showDeleted = false): int {
$getPosts = \Misuzu\DB::prepare(sprintf(
'
SELECT COUNT(p.`post_id`)
FROM `msz_forum_posts` AS p
WHERE `user_id` = :user_id
%1$s
',
$showDeleted ? '' : 'AND `post_deleted` IS NULL'
));
$getPosts->bind('user_id', $userId);
return (int)$getPosts->fetchColumn();
}
function forum_post_listing(
int $topicId,
int $offset = 0,
int $take = 0,
bool $showDeleted = false,
bool $selectAuthor = false
): array {
$hasPagination = $offset >= 0 && $take > 0;
$getPosts = \Misuzu\DB::prepare(sprintf(
'
SELECT
p.`post_id`, p.`post_text`, p.`post_created`, p.`post_parse`,
p.`topic_id`, p.`post_deleted`, p.`post_edited`, p.`post_display_signature`,
INET6_NTOA(p.`post_ip`) AS `post_ip`,
u.`user_id` AS `poster_id`, u.`username` AS `poster_name`,
u.`user_created` AS `poster_joined`, u.`user_country` AS `poster_country`,
u.`user_signature_content` AS `poster_signature_content`, u.`user_signature_parser` AS `poster_signature_parser`,
COALESCE(u.`user_colour`, r.`role_colour`) AS `poster_colour`,
COALESCE(u.`user_title`, r.`role_title`) AS `poster_title`,
(
SELECT COUNT(`post_id`)
FROM `msz_forum_posts`
WHERE `user_id` = p.`user_id`
AND `post_deleted` IS NULL
) AS `poster_post_count`,
(
SELECT MIN(`post_id`) = p.`post_id`
FROM `msz_forum_posts`
WHERE `topic_id` = p.`topic_id`
) AS `is_opening_post`,
(
SELECT `user_id` = u.`user_id`
FROM `msz_forum_posts`
WHERE `topic_id` = p.`topic_id`
ORDER BY `post_id`
LIMIT 1
) AS `is_original_poster`
FROM `msz_forum_posts` AS p
LEFT JOIN `msz_users` AS u
ON u.`user_id` = p.`user_id`
LEFT JOIN `msz_roles` AS r
ON r.`role_id` = u.`display_role`
WHERE %3$s = :topic_id
%1$s
ORDER BY `post_id`
%2$s
',
$showDeleted ? '' : 'AND `post_deleted` IS NULL',
$hasPagination ? 'LIMIT :offset, :take' : '',
$selectAuthor ? 'p.`user_id`' : 'p.`topic_id`'
));
$getPosts->bind('topic_id', $topicId);
if($hasPagination) {
$getPosts->bind('offset', $offset);
$getPosts->bind('take', $take);
}
return $getPosts->fetchAll();
}
define('MSZ_E_FORUM_POST_DELETE_OK', 0); // deleting is fine
define('MSZ_E_FORUM_POST_DELETE_USER', 1); // invalid user
define('MSZ_E_FORUM_POST_DELETE_POST', 2); // post doesn't exist
define('MSZ_E_FORUM_POST_DELETE_DELETED', 3); // post is already marked as deleted
define('MSZ_E_FORUM_POST_DELETE_OWNER', 4); // you may only delete your own posts
define('MSZ_E_FORUM_POST_DELETE_OLD', 5); // posts has existed for too long to be deleted
define('MSZ_E_FORUM_POST_DELETE_PERM', 6); // you aren't allowed to delete posts
define('MSZ_E_FORUM_POST_DELETE_OP', 7); // this is the opening post of a topic
// only allow posts made within a week of posting to be deleted by normal users
define('MSZ_FORUM_POST_DELETE_LIMIT', 60 * 60 * 24 * 7);
// set $userId to null for system request, make sure this is NEVER EVER null on user request
// $postId can also be a the return value of forum_post_get if you already grabbed it once before
function forum_post_can_delete($postId, ?int $userId = null): int {
if($userId !== null && $userId < 1) {
return MSZ_E_FORUM_POST_DELETE_USER;
}
if(is_array($postId)) {
$post = $postId;
} else {
$post = forum_post_get((int)$postId, true);
}
if(empty($post)) {
return MSZ_E_FORUM_POST_DELETE_POST;
}
$isSystemReq = $userId === null;
$perms = $isSystemReq ? 0 : forum_perms_get_user($post['forum_id'], $userId)[MSZ_FORUM_PERMS_GENERAL];
$canDeleteAny = $isSystemReq ? true : perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST);
$canViewPost = $isSystemReq ? true : perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM);
$postIsDeleted = !empty($post['post_deleted']);
if(!$canViewPost) {
return MSZ_E_FORUM_POST_DELETE_POST;
}
if($post['is_opening_post']) {
return MSZ_E_FORUM_POST_DELETE_OP;
}
if($postIsDeleted) {
return $canDeleteAny ? MSZ_E_FORUM_POST_DELETE_DELETED : MSZ_E_FORUM_POST_DELETE_POST;
}
if($isSystemReq) {
return MSZ_E_FORUM_POST_DELETE_OK;
}
if(!$canDeleteAny) {
if(!perms_check($perms, MSZ_FORUM_PERM_DELETE_POST)) {
return MSZ_E_FORUM_POST_DELETE_PERM;
}
if($post['poster_id'] !== $userId) {
return MSZ_E_FORUM_POST_DELETE_OWNER;
}
if(strtotime($post['post_created']) <= time() - MSZ_FORUM_POST_DELETE_LIMIT) {
return MSZ_E_FORUM_POST_DELETE_OLD;
}
}
return MSZ_E_FORUM_POST_DELETE_OK;
}
function forum_post_delete(int $postId): bool {
if($postId < 1) {
return false;
}
$markDeleted = \Misuzu\DB::prepare('
UPDATE `msz_forum_posts`
SET `post_deleted` = NOW()
WHERE `post_id` = :post
AND `post_deleted` IS NULL
');
$markDeleted->bind('post', $postId);
return $markDeleted->execute();
}
function forum_post_restore(int $postId): bool {
if($postId < 1) {
return false;
}
$markDeleted = \Misuzu\DB::prepare('
UPDATE `msz_forum_posts`
SET `post_deleted` = NULL
WHERE `post_id` = :post
AND `post_deleted` IS NOT NULL
');
$markDeleted->bind('post', $postId);
return $markDeleted->execute();
}
function forum_post_nuke(int $postId): bool {
if($postId < 1) {
return false;
}
$markDeleted = \Misuzu\DB::prepare('
DELETE FROM `msz_forum_posts`
WHERE `post_id` = :post
');
$markDeleted->bind('post', $postId);
return $markDeleted->execute();
}

View file

@ -1,56 +0,0 @@
<?php
function forum_topic_views_increment(int $topicId): void {
if($topicId < 1) {
return;
}
$bumpViews = \Misuzu\DB::prepare('
UPDATE `msz_forum_topics`
SET `topic_count_views` = `topic_count_views` + 1
WHERE `topic_id` = :topic_id
');
$bumpViews->bind('topic_id', $topicId);
$bumpViews->execute();
}
function forum_topic_mark_read(int $userId, int $topicId, int $forumId): void {
if($userId < 1) {
return;
}
// previously a TRIGGER was used to achieve this behaviour,
// but those explode when running on a lot of queries (like forum_mark_read() does)
// so instead we get to live with this garbage now
// JUST TO CLARIFY: "this behaviour" refers to forum_topic_views_increment only being executed when the topic is viewed for the first time
try {
$markAsRead = \Misuzu\DB::prepare('
INSERT INTO `msz_forum_topics_track`
(`user_id`, `topic_id`, `forum_id`, `track_last_read`)
VALUES
(:user_id, :topic_id, :forum_id, NOW())
');
$markAsRead->bind('user_id', $userId);
$markAsRead->bind('topic_id', $topicId);
$markAsRead->bind('forum_id', $forumId);
if($markAsRead->execute()) {
forum_topic_views_increment($topicId);
}
} catch(PDOException $ex) {
if($ex->getCode() != '23000') {
throw $ex;
}
$markAsRead = \Misuzu\DB::prepare('
UPDATE `msz_forum_topics_track`
SET `track_last_read` = NOW(),
`forum_id` = :forum_id
WHERE `user_id` = :user_id
AND `topic_id` = :topic_id
');
$markAsRead->bind('user_id', $userId);
$markAsRead->bind('topic_id', $topicId);
$markAsRead->bind('forum_id', $forumId);
$markAsRead->execute();
}
}

View file

@ -10,6 +10,7 @@ use Misuzu\HasRankInterface;
use Misuzu\Memoizer; use Misuzu\Memoizer;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\TOTP; use Misuzu\TOTP;
use Misuzu\Forum\ForumCategory;
use Misuzu\Net\IPAddress; use Misuzu\Net\IPAddress;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Misuzu\Users\Assets\UserAvatarAsset; use Misuzu\Users\Assets\UserAvatarAsset;

View file

@ -156,7 +156,7 @@
<a href="{{ url('forum-category', {'forum': category.id}) }}" class="forum__category__link"></a> <a href="{{ url('forum-category', {'forum': category.id}) }}" class="forum__category__link"></a>
<div class="forum__category__container"> <div class="forum__category__container">
<div class="forum__category__icon forum__category__icon--{{ category.unread(user) ? 'un' : '' }}read"> <div class="forum__category__icon forum__category__icon--{{ user is null or category.hasRead(user) ? '' : 'un' }}read">
<span class="{{ category.icon }}"></span> <span class="{{ category.icon }}"></span>
</div> </div>
@ -176,7 +176,7 @@
{% for child in category.children %} {% for child in category.children %}
{% if child.canView(user) %} {% if child.canView(user) %}
<a href="{{ url('forum-category', {'forum': child.id}) }}" <a href="{{ url('forum-category', {'forum': child.id}) }}"
class="forum__category__subforum{% if child.unread(user) %} forum__category__subforum--unread{% endif %}"> class="forum__category__subforum{% if user is not null and not child.hasRead(user) %} forum__category__subforum--unread{% endif %}">
{{ child.name }} {{ child.name }}
</a> </a>
{% endif %} {% endif %}
@ -401,7 +401,7 @@
<a href="{{ url('forum-topic', {'topic': topic.id}) }}" class="forum__topic__link"></a> <a href="{{ url('forum-topic', {'topic': topic.id}) }}" class="forum__topic__link"></a>
<div class="forum__topic__container"> <div class="forum__topic__container">
<div class="forum__topic__icon forum__topic__icon--{{ topic.hasUnread(user) ? 'un' : '' }}read{% if topic.hasPriorityVoting %} forum__topic__icon--wide{% endif %}"> <div class="forum__topic__icon forum__topic__icon--{{ user is null or topic.hasRead(user) ? '' : 'un' }}read{% if topic.hasPriorityVoting %} forum__topic__icon--wide{% endif %}">
<i class="{{ topic.icon(user) }}{% if topic.hasPriorityVoting %} forum__topic__icon--faded{% endif %}"></i> <i class="{{ topic.icon(user) }}{% if topic.hasPriorityVoting %} forum__topic__icon--faded{% endif %}"></i>
{% if topic.hasPriorityVoting %} {% if topic.hasPriorityVoting %}
@ -727,7 +727,7 @@
perms|perms_check(constant('MSZ_FORUM_PERM_DELETE_ANY_POST')) or ( perms|perms_check(constant('MSZ_FORUM_PERM_DELETE_ANY_POST')) or (
user_id == post.poster_id user_id == post.poster_id
and perms|perms_check(constant('MSZ_FORUM_PERM_DELETE_POST')) and perms|perms_check(constant('MSZ_FORUM_PERM_DELETE_POST'))
and post.post_created|date('U') > ''|date('U') - constant('MSZ_FORUM_POST_DELETE_LIMIT') and post.post_created|date('U') > ''|date('U') - constant('\\Misuzu\\Forum\\ForumPost::DELETE_AGE_LIMIT')
) )
) %} ) %}

View file

@ -5,7 +5,7 @@
{% set title = 'Posting' %} {% set title = 'Posting' %}
{% set is_reply = posting_topic is defined %} {% set is_reply = posting_topic is defined %}
{% set is_opening = not is_reply or posting_post.is_opening_post|default(false) %} {% set is_opening = not is_reply or posting_post.isOpeningPost|default(false) %}
{% block content %} {% block content %}
<form method="post" action="{{ url('forum-' ~ (is_reply ? 'post' : 'topic') ~ '-create') }}"> <form method="post" action="{{ url('forum-' ~ (is_reply ? 'post' : 'topic') ~ '-create') }}">
@ -32,7 +32,7 @@
) }} ) }}
{% if posting_post is defined %} {% if posting_post is defined %}
{{ input_hidden('post[id]', posting_post.post_id) }} {{ input_hidden('post[id]', posting_post.id) }}
{% endif %} {% endif %}
{% if posting_notices|length > 0 %} {% if posting_notices|length > 0 %}
@ -45,21 +45,21 @@
</div> </div>
{% endif %} {% endif %}
<div class="container forum__post js-forum-posting" style="{{ posting_post.poster_colour|default(current_user.colour.raw)|html_colour('--accent-colour') }}"> <div class="container forum__post js-forum-posting" style="{{ posting_post.user.colour.raw|default(current_user.colour.raw)|html_colour('--accent-colour') }}">
<div class="forum__post__info"> <div class="forum__post__info">
<div class="forum__post__info__background"></div> <div class="forum__post__info__background"></div>
<div class="forum__post__info__content"> <div class="forum__post__info__content">
<span class="forum__post__avatar">{{ avatar(posting_post.poster_id|default(current_user.id), 120, posting_post.poster_name|default(current_user.username)) }}</span> <span class="forum__post__avatar">{{ avatar(posting_post.user.id|default(current_user.id), 120, posting_post.user.username|default(current_user.username)) }}</span>
<span class="forum__post__username">{{ posting_post.poster_name|default(current_user.username) }}</span> <span class="forum__post__username">{{ posting_post.user.username|default(current_user.username) }}</span>
<div class="forum__post__icons"> <div class="forum__post__icons">
<div class="flag flag--{{ posting_post.poster_country|default(posting_user.country)|lower }}" title="{{ posting_post.poster_country|default(posting_user.country)|country_name }}"></div> <div class="flag flag--{{ posting_post.user.country|default(posting_user.country)|lower }}" title="{{ posting_post.user.country|default(posting_user.country)|country_name }}"></div>
<div class="forum__post__posts-count">{{ posting_post.poster_post_count|default(posting_user.forumPostCount)|number_format }} posts</div> <div class="forum__post__posts-count">{{ posting_post.user.forumPostCount|default(posting_user.forumPostCount)|number_format }} posts</div>
</div> </div>
<div class="forum__post__joined"> <div class="forum__post__joined">
joined <time datetime="{{ posting_post.poster_joined|default(posting_user.createdTime)|date('c') }}" title="{{ posting_post.poster_joined|default(posting_user.createdTime)|date('r') }}">{{ posting_post.poster_joined|default(posting_user.createdTime)|time_diff }}</time> joined <time datetime="{{ posting_post.user.createdTime|default(posting_user.createdTime)|date('c') }}" title="{{ posting_post.user.createdTime|default(posting_user.createdTime)|date('r') }}">{{ posting_post.user.createdTime|default(posting_user.createdTime)|time_diff }}</time>
</div> </div>
</div> </div>
</div> </div>
@ -77,7 +77,7 @@
</span> </span>
</div> </div>
<textarea name="post[text]" class="forum__post__text forum__post__text--edit js-forum-posting-text js-ctrl-enter-submit" placeholder="Type your post content here...">{{ posting_defaults.text|default(posting_post.post_text|default('')) }}</textarea> <textarea name="post[text]" class="forum__post__text forum__post__text--edit js-forum-posting-text js-ctrl-enter-submit" placeholder="Type your post content here...">{{ posting_defaults.text|default(posting_post.body|default('')) }}</textarea>
<div class="forum__post__text js-forum-posting-preview" hidden></div> <div class="forum__post__text js-forum-posting-preview" hidden></div>
<div class="forum__post__actions forum__post__actions--bbcode" hidden> <div class="forum__post__actions forum__post__actions--bbcode" hidden>
@ -142,7 +142,7 @@
{{ input_select( {{ input_select(
'post[parser]', 'post[parser]',
constant('\\Misuzu\\Parsers\\Parser::NAMES'), constant('\\Misuzu\\Parsers\\Parser::NAMES'),
posting_defaults.parser|default(posting_post.post_parse|default(posting_user.preferredParser)), posting_defaults.parser|default(posting_post.bodyParser|default(posting_user.preferredParser)),
null, null, false, 'forum__post__dropdown js-forum-posting-parser' null, null, false, 'forum__post__dropdown js-forum-posting-parser'
) }} ) }}
{% if is_opening and posting_types|length > 1 %} {% if is_opening and posting_types|length > 1 %}
@ -158,8 +158,8 @@
'Display Signature', 'Display Signature',
posting_defaults.signature is not null posting_defaults.signature is not null
? posting_defaults.signature : ( ? posting_defaults.signature : (
posting_post.post_display_signature is defined posting_post.shouldDisplaySignature is defined
? posting_post.post_display_signature ? posting_post.shouldDisplaySignature
: true : true
) )
) }} ) }}