// 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/url.php';
require_once 'src/Forum/perms.php';
require_once 'src/Forum/forum.php';
require_once 'src/Forum/post.php';
require_once 'src/Forum/topic.php';
$dbConfig = parse_ini_file(MSZ_CONFIG . '/config.ini', true, INI_SCANNER_TYPED);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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\Pagination;
use Misuzu\TOTP;
use Misuzu\Forum\ForumCategory;
use Misuzu\Net\IPAddress;
use Misuzu\Parsers\Parser;
use Misuzu\Users\Assets\UserAvatarAsset;

View file

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

View file

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