From 364cc75b68d0a790e94160387fbbc9086d4a10d6 Mon Sep 17 00:00:00 2001 From: flashwave Date: Thu, 10 Jan 2019 21:08:37 +0100 Subject: [PATCH] Added basic post deletion (no AJAX or topic deletion yet). --- assets/less/classes/forum/confirm.less | 18 ++ assets/less/main.less | 1 + public/forum/post.php | 247 +++++++++++++++++++++++++ public/forum/posting.php | 3 +- src/Forum/post.php | 117 +++++++++++- src/csrf.php | 16 ++ templates/forum/confirm.twig | 24 +++ templates/forum/macros.twig | 37 ++-- 8 files changed, 445 insertions(+), 18 deletions(-) create mode 100644 assets/less/classes/forum/confirm.less create mode 100644 public/forum/post.php create mode 100644 templates/forum/confirm.twig diff --git a/assets/less/classes/forum/confirm.less b/assets/less/classes/forum/confirm.less new file mode 100644 index 00000000..43d41976 --- /dev/null +++ b/assets/less/classes/forum/confirm.less @@ -0,0 +1,18 @@ +.forum__confirm { + max-width: 400px; + margin: 0 auto; + + &__message { + padding: 2px 5px; + } + + &__buttons { + display: flex; + padding: 5px; + justify-content: center; + } + + &__button { + margin-right: 5px; + } +} diff --git a/assets/less/main.less b/assets/less/main.less index b589f15c..649d8304 100644 --- a/assets/less/main.less +++ b/assets/less/main.less @@ -166,6 +166,7 @@ html { @import "classes/forum/actions"; @import "classes/forum/categories"; @import "classes/forum/category"; +@import "classes/forum/confirm"; @import "classes/forum/post"; @import "classes/forum/topic"; @import "classes/forum/topics"; diff --git a/public/forum/post.php b/public/forum/post.php new file mode 100644 index 00000000..5c4b7906 --- /dev/null +++ b/public/forum/post.php @@ -0,0 +1,247 @@ + 0) { + echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403); + return; +} +if (user_warning_check_expiration($currentUserId, MSZ_WARN_SILENCE) > 0) { + echo render_info_or_json($isXHR, 'You have been silenced, check your profile for more information.', 403); + return; +} + +if ($isXHR) { + if (!$postRequestVerified) { + http_response_code(403); + echo json_encode([ + 'success' => false, + 'message' => 'Possible request forgery detected.', + ]); + return; + } + + header(csrf_http_header('forum_post')); +} + +$postInfo = forum_post_get($postId, true); +$perms = empty($postInfo) ? 0 : forum_perms_get_user(MSZ_FORUM_PERMS_GENERAL, $postInfo['forum_id'], $currentUserId); + +switch ($postMode) { + case 'delete': + $canDelete = forum_post_can_delete($postInfo, $currentUserId); + $canDeleteMsg = ''; + $responseCode = 200; + + 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 ($isXHR) { + http_response_code($responseCode); + echo json_encode([ + 'success' => false, + 'post_id' => $postInfo['post_id'], + 'code' => $canDelete, + 'message' => $canDeleteMsg, + ]); + break; + } + + echo render_info($canDeleteMsg, $responseCode); + break; + } + + if (!$isXHR) { + if ($postRequestVerified && isset($_GET['confirm']) && $_GET['confirm'] !== '1') { + header("Location: /forum/topic.php?p={$postInfo['post_id']}#p{$postInfo['post_id']}"); + break; + } elseif (!$postRequestVerified) { + echo tpl_render('forum.confirm', [ + 'title' => 'Confirm post deletion', + 'class' => 'far fa-trash-alt', + 'mode' => 'delete', + 'message' => sprintf('You are about to delete post #%d. Are you sure about that?', $postInfo['post_id']), + 'post' => $postInfo, + ]); + break; + } + } + + $deletePost = forum_post_delete($postInfo['post_id']); + + if ($isXHR) { + echo json_encode([ + 'success' => $deletePost, + 'post_id' => $postInfo['post_id'], + 'message' => $deletePost ? 'Post deleted!' : 'Failed to delete post.', + ]); + break; + } + + if (!$deletePost) { + echo render_error(500); + break; + } + + header('Location: /forum/topic.php?t=' . $postInfo['topic_id']); + break; + + case 'nuke': + if (!perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST)) { + echo render_error(403); + break; + } + + if (!$isXHR) { + if ($postRequestVerified && isset($_GET['confirm']) && $_GET['confirm'] !== '1') { + header("Location: /forum/topic.php?p={$postInfo['post_id']}#p{$postInfo['post_id']}"); + break; + } elseif (!$postRequestVerified) { + echo tpl_render('forum.confirm', [ + 'title' => 'Confirm post nuke', + 'class' => 'fas fa-radiation', + 'mode' => 'nuke', + 'message' => sprintf('You are about to PERMANENTLY DELETE post #%d. Are you sure about that?', $postInfo['post_id']), + 'post' => $postInfo, + ]); + break; + } + } + + $nukePost = forum_post_nuke($postInfo['post_id']); + + if (!$nukePost) { + echo render_error(500); + break; + } + + http_response_code(204); + + if (!$isXHR) { + header('Location: /forum/topic.php?t=' . $postInfo['topic_id']); + } + break; + + case 'restore': + if (!perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST)) { + echo render_error(403); + break; + } + + if (!$isXHR) { + if ($postRequestVerified && isset($_GET['confirm']) && $_GET['confirm'] !== '1') { + header("Location: /forum/topic.php?p={$postInfo['post_id']}#p{$postInfo['post_id']}"); + break; + } elseif (!$postRequestVerified) { + echo tpl_render('forum.confirm', [ + 'title' => 'Confirm post restore', + 'class' => 'fas fa-magic', + 'mode' => 'restore', + 'message' => sprintf('You are about to restore post #%d. Are you sure about that?', $postInfo['post_id']), + 'post' => $postInfo, + ]); + break; + } + } + + $restorePost = forum_post_restore($postInfo['post_id']); + + if (!$restorePost) { + echo render_error(500); + break; + } + + http_response_code(204); + + if (!$isXHR) { + header('Location: /forum/topic.php?t=' . $postInfo['topic_id']); + } + break; + + default: // function as an alt for topic.php?p= by default + if (!empty($postInfo['post_deleted']) && !perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST)) { + echo render_error(404); + break; + } + + $postFind = forum_post_find($postInfo['post_id'], user_session_current('user_id', 0)); + + if (empty($postFind)) { + echo render_error(404); + break; + } + + if ($isXHR) { + unset($postFind['can_view_deleted']); + echo json_encode($postFind); + break; + } + + header('Location: ' . url_construct('/forum/topic.php', [ + 't' => $postFind['topic_id'], + 'p' => floor($postFind['preceeding_post_count'] / MSZ_FORUM_POSTS_PER_PAGE) + 1, + ])); +} diff --git a/public/forum/posting.php b/public/forum/posting.php index 1be34891..12714136 100644 --- a/public/forum/posting.php +++ b/public/forum/posting.php @@ -12,8 +12,7 @@ if (user_warning_check_restriction(user_session_current('user_id', 0))) { } $forumPostingModes = [ - 'create', 'edit', 'quote', - 'delete', 'restore', 'nuke', + 'create', 'edit', 'quote', ]; if (!empty($_POST)) { diff --git a/src/Forum/post.php b/src/Forum/post.php index 0c7d6584..0f7686a4 100644 --- a/src/Forum/post.php +++ b/src/Forum/post.php @@ -86,7 +86,7 @@ function forum_post_get(int $postId, bool $allowDeleted = false): array ' SELECT p.`post_id`, p.`post_text`, p.`post_created`, p.`post_parse`, - p.`topic_id`, p.`post_deleted`, p.`post_edited`, + 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`, @@ -166,3 +166,118 @@ function forum_post_listing(int $topicId, int $offset = 0, int $take = 0, bool $ return db_fetch_all($getPosts); } + +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) || $postId < 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(MSZ_FORUM_PERMS_GENERAL, $post['forum_id'], $userId); + $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 = db_prepare(' + UPDATE `msz_forum_posts` + SET `post_deleted` = NOW() + WHERE `post_id` = :post + AND `post_deleted` IS NULL + '); + $markDeleted->bindValue('post', $postId); + return $markDeleted->execute(); +} + +function forum_post_restore(int $postId): bool +{ + if ($postId < 1) { + return false; + } + + $markDeleted = db_prepare(' + UPDATE `msz_forum_posts` + SET `post_deleted` = NULL + WHERE `post_id` = :post + AND `post_deleted` IS NOT NULL + '); + $markDeleted->bindValue('post', $postId); + return $markDeleted->execute(); +} + +function forum_post_nuke(int $postId): bool +{ + if ($postId < 1) { + return false; + } + + $markDeleted = db_prepare(' + DELETE FROM `msz_forum_posts` + WHERE `post_id` = :post + '); + $markDeleted->bindValue('post', $postId); + return $markDeleted->execute(); +} diff --git a/src/csrf.php b/src/csrf.php index b2bbdc15..c1db6cdd 100644 --- a/src/csrf.php +++ b/src/csrf.php @@ -120,6 +120,22 @@ function csrf_http_header(string $realm, string $name = 'X-Misuzu-CSRF'): string return "{$name}: {$realm};" . csrf_token($realm); } +function csrf_http_header_parse(string $header): array +{ + $split = explode(';', $header, 2); + $realm = $split[0] ?? ''; + $token = $split[1] ?? ''; + + if (empty($realm) || empty($token)) { + [$realm, $token] = ['', '']; + } + + return [ + 'realm' => $realm, + 'token' => $token, + ]; +} + function csrf_get_list(): array { $list = []; diff --git a/templates/forum/confirm.twig b/templates/forum/confirm.twig new file mode 100644 index 00000000..e4bbf6bb --- /dev/null +++ b/templates/forum/confirm.twig @@ -0,0 +1,24 @@ +{% extends 'forum/master.twig' %} +{% from 'forum/macros.twig' import forum_category_listing, forum_topic_listing, forum_category_buttons, forum_header, forum_category_tools %} +{% from 'macros.twig' import container_title %} +{% from '_layout/input.twig' import input_csrf %} + +{% set title = title|default('Confirm your action') %} + +{% block content %} +
+ {{ container_title(' ' ~ title) }} + {{ input_csrf('forum_post') }} + + + +
+ {{ message|default('Are you sure you w') }} +
+ +
+ + +
+
+{% endblock %} diff --git a/templates/forum/macros.twig b/templates/forum/macros.twig index b9eccebf..962d6a0d 100644 --- a/templates/forum/macros.twig +++ b/templates/forum/macros.twig @@ -350,17 +350,24 @@ {% from _self import forum_post_entry %} {% for post in posts %} - {{ forum_post_entry( - post, - perms|perms_check(constant('MSZ_FORUM_PERM_CREATE_POST')), - perms|perms_check(constant(user_id == post.poster_id ? 'MSZ_FORUM_PERM_EDIT_POST' : 'MSZ_FORUM_PERM_EDIT_ANY_POST')), - perms|perms_check(constant(user_id == post.poster_id ? 'MSZ_FORUM_PERM_DELETE_POST' : 'MSZ_FORUM_PERM_DELETE_ANY_POST')) - ) }} + {{ forum_post_entry(post, user_id, perms) }} {% endfor %} {% endmacro %} -{% macro forum_post_entry(post, can_post, can_edit, can_delete) %} - {% set is_deleted = post.post_deleted is not null %} +{% macro forum_post_entry(post, user_id, perms) %} + {% set is_deleted = post.post_deleted is not null %} + {% set can_post = perms|perms_check(constant('MSZ_FORUM_PERM_CREATE_POST')) %} + {% set can_edit = perms|perms_check(constant('MSZ_FORUM_PERM_EDIT_ANY_POST')) or ( + user_id == post.poster_id + and perms|perms_check(constant('MSZ_FORUM_PERM_EDIT_POST')) + ) %} + {% set can_delete = not post.is_opening_post and ( + perms|perms_check(constant('MSZ_FORUM_PERM_DELETE_ANY_POST')) or ( + user_id == post.poster_id + and perms|perms_check(constant('MSZ_FORUM_PERM_DELETE_POST')) + and post.post_created|date('U') > ''|date('U') - constant('MSZ_FORUM_POST_DELETE_LIMIT') + ) + ) %}