Replaced confirm pages with dynamic requests on the forum.

This commit is contained in:
flash 2024-12-18 03:07:48 +00:00
parent 3d8d0b7e88
commit 308ba33377
18 changed files with 1159 additions and 437 deletions

View file

@ -164,6 +164,9 @@
transition: background-color .2s;
border-radius: 3px;
cursor: pointer;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.forum__post__action:hover,
.forum__post__action:focus {

View file

@ -1,3 +1,4 @@
#include msgbox.jsx
#include utility.js
#include xhr.js
#include embed/embed.js
@ -64,12 +65,73 @@
});
};
const initXhrActions = () => {
const targets = Array.from($qa('a[data-url], button[data-url]'));
for(const target of targets) {
target.onclick = async () => {
if(target.disabled)
return;
const url = target.dataset.url;
if(typeof url !== 'string' || url.length < 1)
return;
const disableWithTarget = typeof target.dataset.disableWithTarget === 'string'
? (target.querySelector(target.dataset.disableWithTarget) ?? target)
: target;
const originalText = disableWithTarget.textContent;
try {
target.disabled = true;
if(target.dataset.disableWith)
disableWithTarget.textContent = target.dataset.disableWith;
if(target.dataset.confirm && !await MszShowConfirmBox(target.dataset.confirm, 'Are you sure?'))
return;
const { status, body } = await $x.send(
target.dataset.method ?? 'GET',
url,
{
type: 'json',
authed: target.dataset.withAuth,
csrf: target.dataset.withCsrf,
}
)
if(status >= 400)
await MszShowMessageBox(
body?.error?.text ?? `No additional information was provided. (HTTP ${status})`,
'Failed to complete action'
);
else if(status >= 200 && status <= 299) {
if(target.dataset.refreshOnSuccess)
location.reload();
else {
const redirectUrl = target.dataset.redirectOnSuccess;
if(typeof redirectUrl === 'string' && redirectUrl.startsWith('/') && !redirectUrl.startsWith('//'))
location.assign(redirectUrl);
}
}
} catch(ex) {
console.error(ex);
await MszShowMessageBox(ex, 'Failed to complete action');
} finally {
disableWithTarget.textContent = originalText;
target.disabled = false;
}
};
}
};
try {
MszSakuya.trackElements($qa('time'));
hljs.highlightAll();
MszEmbed.init(`${location.protocol}//uiharu.${location.host}`);
initXhrActions();
// only used by the forum posting form
initQuickSubmit();
const forumPostingForm = $q('.js-forum-posting');

View file

@ -7,46 +7,7 @@ use RuntimeException;
if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext))
die('Script must be called through the Misuzu route dispatcher.');
$mode = (string)filter_input(INPUT_GET, 'm');
$currentUser = $msz->authInfo->userInfo;
$currentUserId = $currentUser === null ? '0' : $currentUser->id;
if($mode === 'mark') {
if(!$msz->authInfo->isLoggedIn)
Template::throwError(403);
$categoryId = filter_input(INPUT_GET, 'f', FILTER_SANITIZE_NUMBER_INT);
if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$categoryInfos = $categoryId === null
? $msz->forumCtx->categories->getCategories()
: $msz->forumCtx->categories->getCategoryChildren(parentInfo: $categoryId, includeSelf: true);
foreach($categoryInfos as $categoryInfo) {
$perms = $msz->authInfo->getPerms('forum', $categoryInfo);
if($perms->check(Perm::F_CATEGORY_LIST))
$msz->forumCtx->categories->updateUserReadCategory($currentUser, $categoryInfo);
}
Tools::redirect($msz->urls->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]));
return;
}
Template::render('confirm', [
'title' => 'Mark forum as read',
'message' => 'Are you sure you want to mark ' . ($categoryId < 1 ? 'the entire' : 'this') . ' forum as read?',
'return' => $msz->urls->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]),
'params' => [
'forum' => $categoryId,
]
]);
return;
}
if($mode !== '')
Template::throwError(404);
$categories = $msz->forumCtx->categories->getCategories(hidden: false, asTree: true);
foreach($categories as $categoryId => $category) {

View file

@ -1,139 +0,0 @@
<?php
namespace Misuzu;
use RuntimeException;
if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext))
die('Script must be called through the Misuzu route dispatcher.');
$postId = !empty($_GET['p']) && is_string($_GET['p']) ? (string)$_GET['p'] : '0';
$postMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1';
$postRequestVerified = CSRF::validateRequest();
if(!empty($postMode) && !$msz->authInfo->isLoggedIn)
Template::displayInfo('You must be logged in to manage posts.', 401);
$currentUser = $msz->authInfo->userInfo;
$currentUserId = $currentUser === null ? '0' : $currentUser->id;
if($postMode !== '' && $msz->usersCtx->hasActiveBan($currentUser))
Template::displayInfo('You have been banned, check your profile for more information.', 403);
try {
$postInfo = $msz->forumCtx->posts->getPost(postId: $postId);
} catch(RuntimeException $ex) {
Template::throwError(404);
}
$perms = $msz->authInfo->getPerms('forum', $postInfo->categoryId);
if(!$perms->check(Perm::F_CATEGORY_VIEW))
Template::throwError(403);
$canDeleteAny = $perms->check(Perm::F_POST_DELETE_ANY);
switch($postMode) {
case 'delete':
if($canDeleteAny) {
if($postInfo->deleted)
Template::displayInfo('This post has already been marked as deleted.', 404);
} else {
if($postInfo->deleted)
Template::throwError(404);
if(!$perms->check(Perm::F_POST_DELETE_OWN))
Template::displayInfo('You are not allowed to delete posts.', 403);
if($postInfo->userId !== $currentUser->id)
Template::displayInfo('You can only delete your own posts.', 403);
// posts may only be deleted within a week of creation, this should be a config value
$deleteTimeFrame = 60 * 60 * 24 * 7;
if($postInfo->createdTime < time() - $deleteTimeFrame)
Template::displayInfo('This post has existed for too long. Ask a moderator to remove if it absolutely necessary.', 403);
}
$originalPostInfo = $msz->forumCtx->posts->getPost(topicInfo: $postInfo->topicId);
if($originalPostInfo->id === $postInfo->id)
Template::displayInfo('This is the opening post of the topic it belongs to, it may not be deleted without deleting the entire topic as well.', 403);
if($postRequestVerified && !$submissionConfirmed) {
Tools::redirect($msz->urls->format('forum-post', ['post' => $postInfo->id]));
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->id),
'params' => [
'p' => $postInfo->id,
'm' => 'delete',
],
]);
break;
}
$msz->forumCtx->posts->deletePost($postInfo);
$msz->createAuditLog('FORUM_POST_DELETE', [$postInfo->id]);
Tools::redirect($msz->urls->format('forum-topic', ['topic' => $postInfo->topicId]));
break;
case 'nuke':
if(!$canDeleteAny)
Template::throwError(403);
if($postRequestVerified && !$submissionConfirmed) {
Tools::redirect($msz->urls->format('forum-post', ['post' => $postInfo->id]));
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->id),
'params' => [
'p' => $postInfo->id,
'm' => 'nuke',
],
]);
break;
}
$msz->forumCtx->posts->nukePost($postInfo->id);
$msz->createAuditLog('FORUM_POST_NUKE', [$postInfo->id]);
Tools::redirect($msz->urls->format('forum-topic', ['topic' => $postInfo->topicId]));
break;
case 'restore':
if(!$canDeleteAny)
Template::throwError(403);
if($postRequestVerified && !$submissionConfirmed) {
Tools::redirect($msz->urls->format('forum-post', ['post' => $postInfo->id]));
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->id),
'params' => [
'p' => $postInfo->id,
'm' => 'restore',
],
]);
break;
}
$msz->forumCtx->posts->restorePost($postInfo->id);
$msz->createAuditLog('FORUM_POST_RESTORE', [$postInfo->id]);
Tools::redirect($msz->urls->format('forum-topic', ['topic' => $postInfo->topicId]));
break;
default: // function as an alt for topic.php?p= by default
Tools::redirect($msz->urls->format('forum-post', ['post' => $postInfo->id]));
break;
}

View file

@ -10,8 +10,6 @@ if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext))
$postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
$topicId = !empty($_GET['t']) && is_string($_GET['t']) ? (int)$_GET['t'] : 0;
$categoryId = null;
$moderationMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1';
$currentUser = $msz->authInfo->userInfo;
$currentUserId = $currentUser === null ? '0' : $currentUser->id;
@ -70,7 +68,7 @@ if($topicIsNuked || $topicIsDeleted) {
}
}
if(empty($topicRedirectInfo))
if(empty($topicRedirectInfo) && !$canDeleteAny)
Template::throwError(404);
}
@ -99,170 +97,6 @@ $canDelete = !$topicIsDeleted && (
)
);
$validModerationModes = [
'delete', 'restore', 'nuke',
'bump', 'lock', 'unlock',
];
if(in_array($moderationMode, $validModerationModes, true)) {
if(!CSRF::validateRequest())
Template::displayInfo("Couldn't verify this request, please refresh the page and try again.", 403);
if(!$msz->authInfo->isLoggedIn)
Template::displayInfo('You must be logged in to manage posts.', 401);
if($msz->usersCtx->hasActiveBan($currentUser))
Template::displayInfo('You have been banned, check your profile for more information.', 403);
switch($moderationMode) {
case 'delete':
if($canDeleteAny) {
if($topicInfo->deleted)
Template::displayInfo('This topic has already been marked as deleted.', 404);
} else {
if($topicInfo->deleted)
Template::throwError(404);
if(!$canDeleteOwn)
Template::displayInfo("You aren't allowed to delete topics.", 403);
if($topicInfo->userId !== $currentUser->id)
Template::displayInfo('You can only delete your own topics.', 403);
// topics may only be deleted within a day of creation, this should be a config value
$deleteTimeFrame = 60 * 60 * 24;
if($topicInfo->createdTime < time() - $deleteTimeFrame)
Template::displayInfo('This topic has existed for too long. Ask a moderator to remove if it absolutely necessary.', 403);
// deleted posts are intentionally included
$topicPostCount = $msz->forumCtx->posts->countPosts(topicInfo: $topicInfo);
if($topicPostCount > $deletePostThreshold)
Template::displayInfo('This topic already has replies, you may no longer delete it. Ask a moderator to remove if it absolutely necessary.', 403);
}
if(!isset($_GET['confirm'])) {
Template::render('forum.confirm', [
'title' => 'Confirm topic deletion',
'class' => 'far fa-trash-alt',
'message' => sprintf('You are about to delete topic #%d. Are you sure about that?', $topicInfo->id),
'params' => [
't' => $topicInfo->id,
'm' => 'delete',
],
]);
break;
} elseif(!$submissionConfirmed) {
Tools::redirect($msz->urls->format(
'forum-topic',
['topic' => $topicInfo->id]
));
break;
}
$msz->forumCtx->topics->deleteTopic($topicInfo->id);
$msz->createAuditLog('FORUM_TOPIC_DELETE', [$topicInfo->id]);
Tools::redirect($msz->urls->format('forum-category', [
'forum' => $categoryInfo->id,
]));
break;
case 'restore':
if(!$canNukeOrRestore)
Template::throwError(403);
if(!isset($_GET['confirm'])) {
Template::render('forum.confirm', [
'title' => 'Confirm topic restore',
'class' => 'fas fa-magic',
'message' => sprintf('You are about to restore topic #%d. Are you sure about that?', $topicInfo->id),
'params' => [
't' => $topicInfo->id,
'm' => 'restore',
],
]);
break;
} elseif(!$submissionConfirmed) {
Tools::redirect($msz->urls->format('forum-topic', [
'topic' => $topicInfo->id,
]));
break;
}
$msz->forumCtx->topics->restoreTopic($topicInfo->id);
$msz->createAuditLog('FORUM_TOPIC_RESTORE', [$topicInfo->id]);
Tools::redirect($msz->urls->format('forum-category', [
'forum' => $categoryInfo->id,
]));
break;
case 'nuke':
if(!$canNukeOrRestore)
Template::throwError(403);
if(!isset($_GET['confirm'])) {
Template::render('forum.confirm', [
'title' => 'Confirm topic nuke',
'class' => 'fas fa-radiation',
'message' => sprintf('You are about to PERMANENTLY DELETE topic #%d. Are you sure about that?', $topicInfo->id),
'params' => [
't' => $topicInfo->id,
'm' => 'nuke',
],
]);
break;
} elseif(!$submissionConfirmed) {
Tools::redirect($msz->urls->format('forum-topic', [
'topic' => $topicInfo->id,
]));
break;
}
$msz->forumCtx->topics->nukeTopic($topicInfo->id);
$msz->createAuditLog('FORUM_TOPIC_NUKE', [$topicInfo->id]);
Tools::redirect($msz->urls->format('forum-category', [
'forum' => $categoryInfo->id,
]));
break;
case 'bump':
if($canBumpTopic) {
$msz->forumCtx->topics->bumpTopic($topicInfo->id);
$msz->createAuditLog('FORUM_TOPIC_BUMP', [$topicInfo->id]);
}
Tools::redirect($msz->urls->format('forum-topic', [
'topic' => $topicInfo->id,
]));
break;
case 'lock':
if($canLockTopic && !$topicIsLocked) {
$msz->forumCtx->topics->lockTopic($topicInfo->id);
$msz->createAuditLog('FORUM_TOPIC_LOCK', [$topicInfo->id]);
}
Tools::redirect($msz->urls->format('forum-topic', [
'topic' => $topicInfo->id,
]));
break;
case 'unlock':
if($canLockTopic && $topicIsLocked) {
$msz->forumCtx->topics->unlockTopic($topicInfo->id);
$msz->createAuditLog('FORUM_TOPIC_UNLOCK', [$topicInfo->id]);
}
Tools::redirect($msz->urls->format('forum-topic', [
'topic' => $topicInfo->id,
]));
break;
}
return;
}
$topicPosts = $topicInfo->postsCount;
if($canDeleteAny)
$topicPosts += $topicInfo->deletedPostsCount;
@ -331,6 +165,7 @@ Template::render('forum.topic', [
'can_reply' => $canReply,
'topic_pagination' => $topicPagination,
'topic_can_delete' => $canDelete,
'topic_can_delete_any' => $canDeleteAny,
'topic_can_nuke_or_restore' => $canNukeOrRestore,
'topic_can_bump' => $canBumpTopic,
'topic_can_lock' => $canLockTopic,

View file

@ -308,11 +308,10 @@ class ForumCategories {
$query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '=');
$query .= ' ORDER BY forum_parent, forum_order';
$args = 0;
$stmt = $this->cache->get($query);
$stmt->addParameter(++$args, $parentInfo);
$stmt->nextParameter($parentInfo);
if(!$includeSelf)
$stmt->addParameter(++$args, $parentInfo);
$stmt->nextParameter($parentInfo);
$stmt->execute();
$cats = $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...));

View file

@ -0,0 +1,78 @@
<?php
namespace Misuzu\Forum;
use RuntimeException;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpPost,RouteHandler,RouteHandlerTrait};
use Index\Urls\{UrlFormat,UrlSource,UrlSourceTrait};
use Misuzu\{CSRF,Perm};
use Misuzu\Auth\AuthInfo;
class ForumCategoriesRoutes implements RouteHandler, UrlSource {
use RouteHandlerTrait, UrlSourceTrait;
public function __construct(
private ForumContext $forum,
private AuthInfo $authInfo,
) {}
#[HttpPost('/forum/mark-as-read')]
#[UrlFormat('forum-mark-as-read', '/forum/mark-as-read', ['cat' => '<category>', 'rec' => '<recursive>'])]
public function postMarkAsRead(HttpResponseBuilder $response, HttpRequest $request) {
if(!$this->authInfo->isLoggedIn)
return 401;
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
return 403;
$response->setHeader('X-CSRF-Token', CSRF::token());
$catId = (string)$request->getParam('cat', FILTER_SANITIZE_NUMBER_INT);
$recursive = !empty($request->getParam('rec'));
// root category purge must be recursive
if($categoryId === '')
return 400;
if($catId === '')
$cats = $this->forum->categories->getCategories();
elseif($recursive)
$cats = $this->forum->categories->getCategoryChildren(parentInfo: $catId, includeSelf: true);
else
try {
$cats = [$this->forum->categories->getCategory(categoryId: $catId)];
} catch(RuntimeException $ex) {
$cats = [];
}
if(empty($cats)) {
$response->setStatusCode(404);
return [
'error' => [
'name' => 'forum:category:none',
'text' => "Couldn't find that forum category.",
],
];
}
$success = false;
foreach($cats as $category) {
$perms = $this->authInfo->getPerms('forum', $category);
if($perms->check(Perm::F_CATEGORY_LIST)) {
$this->forum->categories->updateUserReadCategory($this->authInfo->userInfo, $category);
$success = true;
}
}
if(!$success) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:category:access',
'text' => "You're not allowed to access this forum category.",
],
];
}
return 204;
}
}

View file

@ -0,0 +1,348 @@
<?php
namespace Misuzu\Forum;
use RuntimeException;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpDelete,HttpGet,HttpPost,RouteHandler,RouteHandlerTrait};
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait};
use Misuzu\{CSRF,Perm};
use Misuzu\AuditLog\AuditLog;
use Misuzu\Auth\AuthInfo;
use Misuzu\Users\UsersContext;
class ForumPostsRoutes implements RouteHandler, UrlSource {
use RouteHandlerTrait, UrlSourceTrait;
public function __construct(
private UrlRegistry $urls,
private ForumContext $forum,
private UsersContext $usersCtx,
private AuditLog $auditLog,
private AuthInfo $authInfo,
) {}
#[HttpGet('/forum/posts/([0-9]+)')]
public function getPost(HttpResponseBuilder $response, HttpRequest $request, string $postId) {
try {
$post = $this->forum->posts->getPost(postId: $postId);
} catch(RuntimeException $ex) {
return 404;
}
$perms = $this->authInfo->getPerms('forum', $post->categoryId);
if(!$perms->check(Perm::F_CATEGORY_VIEW))
return 403;
$canDeleteAny = $perms->check(Perm::F_POST_DELETE_ANY);
if($post->deleted && !$canDeleteAny)
return 404;
$postsCount = $this->forum->posts->countPosts(
topicInfo: $post->topicId,
upToPostInfo: $post,
deleted: $canDeleteAny ? null : false
);
$pageNumber = (int)ceil($postsCount / 10); // epic magic number
return $response->redirect($this->urls->format('forum-topic', [
'topic' => $post->topicId,
'page' => $pageNumber,
'post' => sprintf('p%s', $post->id),
]));
}
#[HttpDelete('/forum/posts/([0-9]+)')]
#[UrlFormat('forum-post-delete', '/forum/posts/<post>')]
public function deletePost(HttpResponseBuilder $response, HttpRequest $request, string $postId) {
if(!$this->authInfo->isLoggedIn)
return 401;
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
return 403;
$response->setHeader('X-CSRF-Token', CSRF::token());
if($this->usersCtx->hasActiveBan($this->authInfo->authInfo)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'user:banned',
'text' => "You aren't allowed to do that while banned.",
],
];
}
try {
$post = $this->forum->posts->getPost(
postId: $postId,
deleted: false,
);
} catch(RuntimeException $ex) {
$response->setStatusCode(404);
return [
'error' => [
'name' => 'forum:post:none',
'text' => "Couldn't find that forum post.",
],
];
}
$perms = $this->authInfo->getPerms('forum', $post->categoryId);
if(!$perms->check(Perm::F_CATEGORY_VIEW)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:post:access',
'text' => "You aren't allowed to access that post.",
],
];
}
if(!$perms->check(Perm::F_POST_DELETE_ANY)) {
$topic = $this->forum->topics->getTopic(postInfo: $post);
if($topic->deleted) {
$response->setStatusCode(404);
return [
'error' => [
'name' => 'forum:post:none',
'text' => "Couldn't find that forum post.",
],
];
}
if($topic->locked) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:post:delete:lock',
'text' => "The forum topic that post belongs is locked.",
],
];
}
if(!$perms->check(Perm::F_POST_DELETE_OWN)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:post:delete:access',
'text' => "You aren't allowed to delete that post.",
],
];
}
if($post->userId !== $this->authInfo->userId) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:post:delete:own',
'text' => "You aren't allowed to delete posts made by other people.",
],
];
}
// posts may only be deleted within a week of creation, this should be a config value
$deleteTimeFrame = 60 * 60 * 24 * 7;
if($post->createdTime < time() - $deleteTimeFrame) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:post:delete:age',
'text' => "This post has existed for too long. Ask a moderator to remove if it absolutely necessary.",
],
];
}
}
$originalPost = $this->forum->posts->getPost(topicInfo: $post->topicId);
if($originalPost->id === $post->id) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:post:delete:opening',
'text' => "This is the opening post of the topic it belongs to, it may not be deleted without deleting the entire topic as well.",
],
];
}
$category = $this->forum->categories->getCategory(postInfo: $post);
if($category->archived) {
$response->setStatusCode(400);
return [
'error' => [
'name' => 'forum:topic:archived',
'text' => "The forum category this topic belongs to is archived.",
],
];
}
$this->forum->posts->deletePost($post);
$this->auditLog->createLog(
$this->authInfo->userInfo,
'FORUM_POST_DELETE',
[$post->id],
$request->getRemoteAddress(),
$request->getCountryCode()
);
return 204;
}
#[HttpPost('/forum/posts/([0-9]+)/nuke')]
#[UrlFormat('forum-post-nuke', '/forum/posts/<post>/nuke')]
public function postPostNuke(HttpResponseBuilder $response, HttpRequest $request, string $postId) {
if(!$this->authInfo->isLoggedIn)
return 401;
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
return 403;
$response->setHeader('X-CSRF-Token', CSRF::token());
if($this->usersCtx->hasActiveBan($this->authInfo->authInfo)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'user:banned',
'text' => "You aren't allowed to do that while banned.",
],
];
}
try {
$post = $this->forum->posts->getPost(
postId: $postId,
deleted: true,
);
} catch(RuntimeException $ex) {
$response->setStatusCode(404);
return [
'error' => [
'name' => 'forum:post:none',
'text' => "Couldn't find that forum post.",
],
];
}
$perms = $this->authInfo->getPerms('forum', $post->categoryId);
if(!$perms->check(Perm::F_CATEGORY_VIEW)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:post:access',
'text' => "You aren't allowed to access that post.",
],
];
}
if(!$perms->check(Perm::F_POST_DELETE_ANY)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:post:nuke:access',
'text' => "You aren't allowed to nuke that post.",
],
];
}
$category = $this->forum->categories->getCategory(postInfo: $post);
if($category->archived) {
$response->setStatusCode(400);
return [
'error' => [
'name' => 'forum:post:archived',
'text' => "The forum category this post belongs to is archived.",
],
];
}
$this->forum->posts->nukePost($post);
$this->auditLog->createLog(
$this->authInfo->userInfo,
'FORUM_POST_NUKE',
[$post->id],
$request->getRemoteAddress(),
$request->getCountryCode()
);
return 204;
}
#[HttpPost('/forum/posts/([0-9]+)/restore')]
#[UrlFormat('forum-post-restore', '/forum/posts/<post>/restore')]
public function postPostRestore(HttpResponseBuilder $response, HttpRequest $request, string $postId) {
if(!$this->authInfo->isLoggedIn)
return 401;
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
return 403;
$response->setHeader('X-CSRF-Token', CSRF::token());
if($this->usersCtx->hasActiveBan($this->authInfo->authInfo)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'user:banned',
'text' => "You aren't allowed to do that while banned.",
],
];
}
try {
$post = $this->forum->posts->getPost(
postId: $postId,
deleted: true,
);
} catch(RuntimeException $ex) {
$response->setStatusCode(404);
return [
'error' => [
'name' => 'forum:post:none',
'text' => "Couldn't find that forum post.",
],
];
}
$perms = $this->authInfo->getPerms('forum', $post->categoryId);
if(!$perms->check(Perm::F_CATEGORY_VIEW)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:post:access',
'text' => "You aren't allowed to access that post.",
],
];
}
if(!$perms->check(Perm::F_POST_DELETE_ANY)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:post:restore:access',
'text' => "You aren't allowed to restore that post.",
],
];
}
$category = $this->forum->categories->getCategory(postInfo: $post);
if($category->archived) {
$response->setStatusCode(400);
return [
'error' => [
'name' => 'forum:post:archived',
'text' => "The forum category this post belongs to is archived.",
],
];
}
$this->forum->posts->restorePost($post);
$this->auditLog->createLog(
$this->authInfo->userInfo,
'FORUM_POST_RESTORE',
[$post->id],
$request->getRemoteAddress(),
$request->getCountryCode()
);
return 204;
}
}

View file

@ -203,10 +203,12 @@ class ForumTopics {
public function getTopic(
?string $topicId = null,
ForumPostInfo|string|null $postInfo = null
ForumPostInfo|string|null $postInfo = null,
?bool $deleted = null
): ForumTopicInfo {
$hasTopicId = $topicId !== null;
$hasPostInfo = $postInfo !== null;
$hasDeleted = $deleted !== null;
if(!$hasTopicId && !$hasPostInfo)
throw new InvalidArgumentException('At least one argument must be specified.');
@ -228,6 +230,8 @@ class ForumTopics {
$value = $postInfo;
}
}
if($hasDeleted)
$query .= sprintf(' AND topic_deleted %s NULL', $deleted ? 'IS NOT' : 'IS');
$stmt = $this->cache->get($query);
$stmt->addParameter(1, $value);

View file

@ -0,0 +1,568 @@
<?php
namespace Misuzu\Forum;
use RuntimeException;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpDelete,HttpGet,HttpPost,RouteHandler,RouteHandlerTrait};
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait};
use Misuzu\{CSRF,Perm};
use Misuzu\AuditLog\AuditLog;
use Misuzu\Auth\AuthInfo;
use Misuzu\Users\UsersContext;
class ForumTopicsRoutes implements RouteHandler, UrlSource {
use RouteHandlerTrait, UrlSourceTrait;
public function __construct(
private UrlRegistry $urls,
private ForumContext $forum,
private UsersContext $usersCtx,
private AuditLog $auditLog,
private AuthInfo $authInfo,
) {}
#[HttpGet('/forum/topics/([0-9]+)')]
public function getTopic(HttpResponseBuilder $response, HttpRequest $request, string $topicId) {
$response->redirect($this->urls->format('forum-topic', ['topic' => $topicId]));
}
#[HttpDelete('/forum/topics/([0-9]+)')]
#[UrlFormat('forum-topic-delete', '/forum/topics/<topic>')]
public function deleteTopic(HttpResponseBuilder $response, HttpRequest $request, string $topicId) {
if(!$this->authInfo->isLoggedIn)
return 401;
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
return 403;
$response->setHeader('X-CSRF-Token', CSRF::token());
if($this->usersCtx->hasActiveBan($this->authInfo->authInfo)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'user:banned',
'text' => "You aren't allowed to do that while banned.",
],
];
}
try {
$topic = $this->forum->topics->getTopic(
topicId: $topicId,
deleted: false,
);
} catch(RuntimeException $ex) {
$response->setStatusCode(404);
return [
'error' => [
'name' => 'forum:topic:none',
'text' => "Couldn't find that forum topic.",
],
];
}
$perms = $this->authInfo->getPerms('forum', $topic->categoryId);
if(!$perms->check(Perm::F_CATEGORY_VIEW)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:access',
'text' => "You aren't allowed to access that topic.",
],
];
}
if(!$perms->check(Perm::F_POST_DELETE_ANY)) {
if($topic->locked || !$perms->check(Perm::F_POST_DELETE_OWN)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:delete:access',
'text' => "You aren't allowed to delete that topic.",
],
];
}
if($topic->userId !== $this->authInfo->userId) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:delete:own',
'text' => "You aren't allowed to delete topics made by other people.",
],
];
}
// topics may only be deleted within a day of creation, this should be a config value
$deleteTimeFrame = 60 * 60 * 24;
if($topic->createdTime < time() - $deleteTimeFrame) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:delete:age',
'text' => "This topic has existed for too long. Ask a moderator to remove if it absolutely necessary.",
],
];
}
// Maximum amount of posts a topic may contain to still be deletable by the author
// this should be in the config
$deletePostThreshold = 1;
// deleted posts are intentionally included
$topicPostCount = $this->forum->posts->countPosts(topicInfo: $topic);
if($topicPostCount > $deletePostThreshold) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:delete:replies',
'text' => "This topic already has replies, you may no longer delete it. Ask a moderator to remove if it absolutely necessary.",
],
];
}
}
$category = $this->forum->categories->getCategory(topicInfo: $topic);
if($category->archived) {
$response->setStatusCode(400);
return [
'error' => [
'name' => 'forum:topic:archived',
'text' => "The forum category this topic belongs to is archived.",
],
];
}
$this->forum->topics->deleteTopic($topic);
$this->auditLog->createLog(
$this->authInfo->userInfo,
'FORUM_TOPIC_DELETE',
[$topic->id],
$request->getRemoteAddress(),
$request->getCountryCode()
);
return 204;
}
#[HttpPost('/forum/topics/([0-9]+)/restore')]
#[UrlFormat('forum-topic-restore', '/forum/topics/<topic>/restore')]
public function postTopicRestore(HttpResponseBuilder $response, HttpRequest $request, string $topicId) {
if(!$this->authInfo->isLoggedIn)
return 401;
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
return 403;
$response->setHeader('X-CSRF-Token', CSRF::token());
if($this->usersCtx->hasActiveBan($this->authInfo->authInfo)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'user:banned',
'text' => "You aren't allowed to do that while banned.",
],
];
}
try {
$topic = $this->forum->topics->getTopic(
topicId: $topicId,
deleted: true,
);
} catch(RuntimeException $ex) {
$response->setStatusCode(404);
return [
'error' => [
'name' => 'forum:topic:none',
'text' => "Couldn't find that forum topic.",
],
];
}
$perms = $this->authInfo->getPerms('forum', $topic->categoryId);
if(!$perms->check(Perm::F_CATEGORY_VIEW)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:access',
'text' => "You aren't allowed to access that topic.",
],
];
}
if(!$perms->check(Perm::F_POST_DELETE_ANY)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:restore:access',
'text' => "You aren't allowed to restore that topic.",
],
];
}
$category = $this->forum->categories->getCategory(topicInfo: $topic);
if($category->archived) {
$response->setStatusCode(400);
return [
'error' => [
'name' => 'forum:topic:archived',
'text' => "The forum category this topic belongs to is archived.",
],
];
}
$this->forum->topics->restoreTopic($topic);
$this->auditLog->createLog(
$this->authInfo->userInfo,
'FORUM_TOPIC_RESTORE',
[$topic->id],
$request->getRemoteAddress(),
$request->getCountryCode()
);
return 204;
}
#[HttpPost('/forum/topics/([0-9]+)/nuke')]
#[UrlFormat('forum-topic-nuke', '/forum/topics/<topic>/nuke')]
public function postTopicNuke(HttpResponseBuilder $response, HttpRequest $request, string $topicId) {
if(!$this->authInfo->isLoggedIn)
return 401;
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
return 403;
$response->setHeader('X-CSRF-Token', CSRF::token());
if($this->usersCtx->hasActiveBan($this->authInfo->authInfo)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'user:banned',
'text' => "You aren't allowed to do that while banned.",
],
];
}
try {
$topic = $this->forum->topics->getTopic(
topicId: $topicId,
deleted: true,
);
} catch(RuntimeException $ex) {
$response->setStatusCode(404);
return [
'error' => [
'name' => 'forum:topic:none',
'text' => "Couldn't find that forum topic.",
],
];
}
$perms = $this->authInfo->getPerms('forum', $topic->categoryId);
if(!$perms->check(Perm::F_CATEGORY_VIEW)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:access',
'text' => "You aren't allowed to access that topic.",
],
];
}
if(!$perms->check(Perm::F_POST_DELETE_ANY)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:nuke:access',
'text' => "You aren't allowed to nuke that topic.",
],
];
}
$category = $this->forum->categories->getCategory(topicInfo: $topic);
if($category->archived) {
$response->setStatusCode(400);
return [
'error' => [
'name' => 'forum:topic:archived',
'text' => "The forum category this topic belongs to is archived.",
],
];
}
$this->forum->topics->nukeTopic($topic);
$this->auditLog->createLog(
$this->authInfo->userInfo,
'FORUM_TOPIC_NUKE',
[$topic->id],
$request->getRemoteAddress(),
$request->getCountryCode()
);
return 204;
}
#[HttpPost('/forum/topics/([0-9]+)/bump')]
#[UrlFormat('forum-topic-bump', '/forum/topics/<topic>/bump')]
public function postTopicBump(HttpResponseBuilder $response, HttpRequest $request, string $topicId) {
if(!$this->authInfo->isLoggedIn)
return 401;
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
return 403;
$response->setHeader('X-CSRF-Token', CSRF::token());
if($this->usersCtx->hasActiveBan($this->authInfo->authInfo)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'user:banned',
'text' => "You aren't allowed to do that while banned.",
],
];
}
try {
$topic = $this->forum->topics->getTopic(
topicId: $topicId,
deleted: false,
);
} catch(RuntimeException $ex) {
$response->setStatusCode(404);
return [
'error' => [
'name' => 'forum:topic:none',
'text' => "Couldn't find that forum topic.",
],
];
}
$perms = $this->authInfo->getPerms('forum', $topic->categoryId);
if(!$perms->check(Perm::F_CATEGORY_VIEW)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:access',
'text' => "You aren't allowed to access that topic.",
],
];
}
if(!$perms->check(Perm::F_TOPIC_BUMP)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:bump:access',
'text' => "You aren't allowed to bump that topic.",
],
];
}
$category = $this->forum->categories->getCategory(topicInfo: $topic);
if($category->archived) {
$response->setStatusCode(400);
return [
'error' => [
'name' => 'forum:topic:archived',
'text' => "The forum category this topic belongs to is archived.",
],
];
}
$this->forum->topics->bumpTopic($topic);
$this->auditLog->createLog(
$this->authInfo->userInfo,
'FORUM_TOPIC_BUMP',
[$topic->id],
$request->getRemoteAddress(),
$request->getCountryCode()
);
return 204;
}
#[HttpPost('/forum/topics/([0-9]+)/lock')]
#[UrlFormat('forum-topic-lock', '/forum/topics/<topic>/lock')]
public function postTopicLock(HttpResponseBuilder $response, HttpRequest $request, string $topicId) {
if(!$this->authInfo->isLoggedIn)
return 401;
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
return 403;
$response->setHeader('X-CSRF-Token', CSRF::token());
if($this->usersCtx->hasActiveBan($this->authInfo->authInfo)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'user:banned',
'text' => "You aren't allowed to do that while banned.",
],
];
}
try {
$topic = $this->forum->topics->getTopic(
topicId: $topicId,
deleted: false,
);
} catch(RuntimeException $ex) {
$response->setStatusCode(404);
return [
'error' => [
'name' => 'forum:topic:none',
'text' => "Couldn't find that forum topic.",
],
];
}
$perms = $this->authInfo->getPerms('forum', $topic->categoryId);
if(!$perms->check(Perm::F_CATEGORY_VIEW)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:access',
'text' => "You aren't allowed to access that topic.",
],
];
}
if(!$perms->check(Perm::F_TOPIC_LOCK)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:lock:access',
'text' => "You aren't allowed to lock that topic.",
],
];
}
if($topic->locked) {
$response->setStatusCode(400);
return [
'error' => [
'name' => 'forum:topic:lock:already',
'text' => "That forum topic has already been locked.",
],
];
}
$category = $this->forum->categories->getCategory(topicInfo: $topic);
if($category->archived) {
$response->setStatusCode(400);
return [
'error' => [
'name' => 'forum:topic:archived',
'text' => "The forum category this topic belongs to is archived.",
],
];
}
$this->forum->topics->lockTopic($topic);
$this->auditLog->createLog(
$this->authInfo->userInfo,
'FORUM_TOPIC_LOCK',
[$topic->id],
$request->getRemoteAddress(),
$request->getCountryCode()
);
return 204;
}
#[HttpPost('/forum/topics/([0-9]+)/unlock')]
#[UrlFormat('forum-topic-unlock', '/forum/topics/<topic>/unlock')]
public function postTopicUnlock(HttpResponseBuilder $response, HttpRequest $request, string $topicId) {
if(!$this->authInfo->isLoggedIn)
return 401;
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
return 403;
$response->setHeader('X-CSRF-Token', CSRF::token());
if($this->usersCtx->hasActiveBan($this->authInfo->authInfo)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'user:banned',
'text' => "You aren't allowed to do that while banned.",
],
];
}
try {
$topic = $this->forum->topics->getTopic(
topicId: $topicId,
deleted: false,
);
} catch(RuntimeException $ex) {
$response->setStatusCode(404);
return [
'error' => [
'name' => 'forum:topic:none',
'text' => "Couldn't find that forum topic.",
],
];
}
$perms = $this->authInfo->getPerms('forum', $topic->categoryId);
if(!$perms->check(Perm::F_CATEGORY_VIEW)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:access',
'text' => "You aren't allowed to access that topic.",
],
];
}
if(!$perms->check(Perm::F_TOPIC_LOCK)) {
$response->setStatusCode(403);
return [
'error' => [
'name' => 'forum:topic:lock:access',
'text' => "You aren't allowed to lock that topic.",
],
];
}
if(!$topic->locked) {
$response->setStatusCode(400);
return [
'error' => [
'name' => 'forum:topic:lock:not',
'text' => "This forum topic hasn't been locked yet.",
],
];
}
$category = $this->forum->categories->getCategory(topicInfo: $topic);
if($category->archived) {
$response->setStatusCode(400);
return [
'error' => [
'name' => 'forum:topic:archived',
'text' => "The forum category this topic belongs to is archived.",
],
];
}
$this->forum->topics->unlockTopic($topic);
$this->auditLog->createLog(
$this->authInfo->userInfo,
'FORUM_TOPIC_UNLOCK',
[$topic->id],
$request->getRemoteAddress(),
$request->getCountryCode()
);
return 204;
}
}

View file

@ -31,25 +31,14 @@ class LegacyRoutes implements RouteHandler, UrlSource {
$urls->register('forum-index', '/forum');
$urls->register('forum-leaderboard', '/forum/leaderboard.php', ['id' => '<id>', 'mode' => '<mode>']);
$urls->register('forum-mark-global', '/forum/index.php', ['m' => 'mark']);
$urls->register('forum-mark-single', '/forum/index.php', ['m' => 'mark', 'f' => '<forum>']);
$urls->register('forum-topic-new', '/forum/posting.php', ['f' => '<forum>']);
$urls->register('forum-reply-new', '/forum/posting.php', ['t' => '<topic>']);
$urls->register('forum-category', '/forum/forum.php', ['f' => '<forum>', 'p' => '<page>']);
$urls->register('forum-category-root', '/forum/index.php', fragment: '<forum>');
$urls->register('forum-topic', '/forum/topic.php', ['t' => '<topic>', 'page' => '<page>']);
$urls->register('forum-topic', '/forum/topic.php', ['t' => '<topic>', 'page' => '<page>'], '<post>');
$urls->register('forum-topic-create', '/forum/posting.php', ['f' => '<forum>']);
$urls->register('forum-topic-bump', '/forum/topic.php', ['t' => '<topic>', 'm' => 'bump', 'csrf' => '<csrf>']);
$urls->register('forum-topic-lock', '/forum/topic.php', ['t' => '<topic>', 'm' => 'lock', 'csrf' => '<csrf>']);
$urls->register('forum-topic-unlock', '/forum/topic.php', ['t' => '<topic>', 'm' => 'unlock', 'csrf' => '<csrf>']);
$urls->register('forum-topic-delete', '/forum/topic.php', ['t' => '<topic>', 'm' => 'delete', 'csrf' => '<csrf>']);
$urls->register('forum-topic-restore', '/forum/topic.php', ['t' => '<topic>', 'm' => 'restore', 'csrf' => '<csrf>']);
$urls->register('forum-topic-nuke', '/forum/topic.php', ['t' => '<topic>', 'm' => 'nuke', 'csrf' => '<csrf>']);
$urls->register('forum-post', '/forum/topic.php', ['p' => '<post>'], 'p<post>');
$urls->register('forum-post-create', '/forum/posting.php', ['t' => '<topic>']);
$urls->register('forum-post-delete', '/forum/post.php', ['p' => '<post>', 'm' => 'delete']);
$urls->register('forum-post-restore', '/forum/post.php', ['p' => '<post>', 'm' => 'restore']);
$urls->register('forum-post-nuke', '/forum/post.php', ['p' => '<post>', 'm' => 'nuke']);
$urls->register('forum-post-quote', '/forum/posting.php', ['q' => '<post>']);
$urls->register('forum-post-edit', '/forum/posting.php', ['p' => '<post>', 'm' => 'edit']);
@ -196,6 +185,13 @@ class LegacyRoutes implements RouteHandler, UrlSource {
), true);
}
#[HttpGet('/forum/post.php')]
public function getForumPostPHP(HttpResponseBuilder $response, HttpRequest $request): void {
$response->redirect($this->urls->format('forum-post', [
'post' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT),
]), true);
}
#[HttpGet('/changelog.php')]
public function getChangelogPHP(HttpResponseBuilder $response, HttpRequest $request): void {
$changeId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);

View file

@ -186,6 +186,25 @@ class MisuzuContext {
$this->perms
));
$routingCtx->register(new \Misuzu\Forum\ForumCategoriesRoutes(
$this->forumCtx,
$this->authInfo,
));
$routingCtx->register(new \Misuzu\Forum\ForumTopicsRoutes(
$this->urls,
$this->forumCtx,
$this->usersCtx,
$this->auditLog,
$this->authInfo,
));
$routingCtx->register(new \Misuzu\Forum\ForumPostsRoutes(
$this->urls,
$this->forumCtx,
$this->usersCtx,
$this->auditLog,
$this->authInfo,
));
$routingCtx->register(new \Misuzu\Changelog\ChangelogRoutes(
$this->siteInfo,
$this->urls,

View file

@ -1,24 +0,0 @@
{% extends 'master.twig' %}
{% from 'macros.twig' import container_title %}
{% from '_layout/input.twig' import input_csrf %}
{% set title = title|default('Confirm your action') %}
{% block content %}
<form action="{{ action|default('') }}" method="{{ method|default('post') }}" class="container confirm">
{{ container_title('<i class="' ~ class|default('fas fa-exclamation-circle') ~ ' fa-fw"></i> ' ~ title) }}
{{ input_csrf() }}
{% for name, value in params|default([]) %}
{% if value is not empty %}
<input type="hidden" name="{{ name }}" value="{{ value }}"/>
{% endif %}
{% endfor %}
<div class="confirm__message">
{{ message|default('Are you sure you w') }}
</div>
<div class="confirm__buttons">
<input type="submit" class="input__button confirm__button" value="Yes">
<a href="{{ return|default('/') }}" class="input__button confirm__button">No</a>
</div>
</form>
{% endblock %}

View file

@ -1,24 +0,0 @@
{% extends 'forum/master.twig' %}
{% from 'macros.twig' import container_title %}
{% from '_layout/input.twig' import input_csrf %}
{% set title = title|default('Confirm your action') %}
{% block content %}
<form action="" method="get" class="container forum__confirm">
{{ container_title('<i class="' ~ class|default('fas fa-exclamation-circle') ~ ' fa-fw"></i> ' ~ title) }}
{{ input_csrf('csrf') }}
{% for name, value in params %}
<input type="hidden" name="{{ name }}" value="{{ value }}">
{% endfor %}
<div class="forum__confirm__message">
{{ message|default('Are you sure you w') }}
</div>
<div class="forum__confirm__buttons">
<button name="confirm" value="1" class="input__button forum__confirm__button">Yes</button>
<button name="confirm" value="0" class="input__button forum__confirm__button">No</button>
</div>
</form>
{% endblock %}

View file

@ -10,10 +10,14 @@
{% block content %}
{{ forum_header(forum_info.name, forum_breadcrumbs, true, canonical_url, [
{
'html': '<i class="far fa-check-circle"></i> Mark as Read',
'url': url('forum-mark-single', {'forum': forum_info.id}),
'display': forum_show_mark_as_read,
'method': 'POST',
html: '<i class="far fa-check-circle"></i> <span class="js-action-text">Mark as Read</span>',
display: forum_show_mark_as_read,
method: 'POST',
url: url('forum-mark-as-read', { category: forum_info.id }),
disableWith: 'Marking as read...',
disableWithTarget: '.js-action-text',
withCsrf: true,
refreshOnSuccess: true,
}
]) }}

View file

@ -13,7 +13,7 @@
{% if forum_show_mark_as_read %}
<div class="container forum__actions">
<a href="{{ url('forum-mark-global') }}" class="input__button forum__actions__button">Mark All Read</a>
<button class="input__button forum__actions__button" type="button" data-method="POST" data-url="{{ url('forum-mark-as-read', { recursive: true }) }}" data-disable-with="Marking as read..." data-with-csrf="1" data-refresh-on-success="1">Mark All Read</button>
</div>
{% endif %}
{% else %}

View file

@ -71,7 +71,7 @@
<div class="forum__header__actions">
{% for action in actions %}
{% if action.display is not defined or action.display %}
<a class="forum__header__action{% if action.class is defined %}{{ action.class }}{% endif %}" href="{{ action.url }}">
<a class="{{ html_classes('forum__header__action', action.class|default('')) }}" {% if action.method is defined %}href="javascript:;" data-method="{{ action.method }}" data-url="{{ action.url }}"{% if action.disableWith is defined %} data-disable-with="{{ action.disableWith }}"{% endif %}{% if action.disableWithTarget is defined %} data-disable-with-target="{{ action.disableWithTarget }}"{% endif %}{% if action.withCsrf|default(false) %} data-with-csrf="1"{% endif %}{% if action.refreshOnSuccess|default(false) %} data-refresh-on-success="1"{% endif %}{% if action.redirectOnSuccess is defined and action.redirectOnSuccess is not empty %} data-redirect-on-success="{{ action.redirectOnSuccess }}"{% endif %}{% if action.confirm is defined %} data-confirm="{{ action.confirm }}"{% endif %}{% else %}href="{{ action.url }}"{% endif %}>
{{ action.html|raw }}
</a>
{% endif %}
@ -549,8 +549,8 @@
{% if perms.can_create_post|default(false) or can_edit or can_delete %}
<div class="forum__post__actions">
{% if post_is_deleted %}
<a href="{{ url('forum-post-restore', {'post': post_id}) }}" class="forum__post__action forum__post__action--restore"><i class="fas fa-magic fa-fw"></i> Restore</a>
<a href="{{ url('forum-post-nuke', {'post': post_id}) }}" class="forum__post__action forum__post__action--nuke"><i class="fas fa-radiation-alt fa-fw"></i> Permanently Delete</a>
<button class="forum__post__action forum__post__action--restore" data-method="POST" data-url="{{ url('forum-post-restore', {'post': post_id}) }}" data-refresh-on-success="1" data-with-csrf="1" data-disable-with="Restoring..." data-disable-with-target=".js-action-text" data-confirm="Are you sure you want to restore this post?"><i class="fas fa-magic fa-fw"></i> <span class="js-action-text">Restore</span></button>
<button class="forum__post__action forum__post__action--nuke" data-method="POST" data-url="{{ url('forum-post-nuke', {'post': post_id}) }}" data-redirect-on-success="{{ url('forum-topic', { topic: post.info.topicId }) }}" data-with-csrf="1" data-disable-with="Nuking..." data-disable-with-target=".js-action-text" data-confirm="Are you sure you want to PERMANENTLY DELETE this post?"><i class="fas fa-radiation-alt fa-fw"></i> <span class="js-action-text">Permanently Delete</span></button>
{% else %}
{# if perms.can_create_post|default(false) %}
<a href="{{ url('forum-post-quote', {'post': post_id}) }}" class="forum__post__action forum__post__action--quote"><i class="fas fa-quote-left fa-fw"></i> Quote</a>
@ -559,7 +559,7 @@
<a href="{{ url('forum-post-edit', {'post': post_id}) }}" class="forum__post__action forum__post__action--edit"><i class="fas fa-edit fa-fw"></i> Edit</a>
{% endif %}
{% if can_delete %}
<a href="{{ url('forum-post-delete', {'post': post_id}) }}" class="forum__post__action forum__post__action--delete"><i class="far fa-trash-alt fa-fw"></i> Delete</a>
<button class="forum__post__action forum__post__action--delete" data-method="DELETE" data-url="{{ url('forum-post-delete', {'post': post_id}) }}" data-redirect-on-success="{{ perms.can_delete_any_post ? url('forum-post', { post: post.info.id }) : url('forum-topic', { topic: post.info.topicId }) }}" data-with-csrf="1" data-disable-with="Deleting..." data-disable-with-target=".js-action-text" data-confirm="Are you sure you want to delete this post?"><i class="fas fa-trash-alt fa-fw"></i> <span class="js-action-text">Delete</span></button>
{% endif %}
{% endif %}
</div>

View file

@ -17,39 +17,71 @@
'page': topic_pagination.page > 1 ? topic_pagination.page : 0,
}) %}
{% set forum_post_csrf = csrf_token() %}
{% set topic_tools = forum_topic_tools(topic_info, topic_pagination, can_reply) %}
{% set topic_notice = forum_topic_locked(topic_info.lockedTime, category_info.archived) ~ forum_topic_redirect(topic_redir_info|default(null)) %}
{% set topic_actions = [
{
'html': '<i class="far fa-trash-alt fa-fw"></i> Delete',
'url': url('forum-topic-delete', { topic: topic_info.id, csrf: csrf_token() }),
'display': topic_can_delete,
html: '<i class="fas fa-trash-alt fa-fw"></i> <span class="js-action-text">Delete</span>',
display: topic_can_delete,
method: 'DELETE',
url: url('forum-topic-delete', { topic: topic_info.id }),
confirm: 'Are you sure you want to delete this topic?',
disableWith: 'Deleting...',
disableWithTarget: '.js-action-text',
withCsrf: true,
refreshOnSuccess: topic_can_delete_any,
redirectOnSuccess: (topic_can_delete_any ? false : url('forum-category', { forum: category_info.id })),
},
{
'html': '<i class="fas fa-magic fa-fw"></i> Restore',
'url': url('forum-topic-restore', { topic: topic_info.id, csrf: csrf_token() }),
'display': topic_can_nuke_or_restore,
html: '<i class="fas fa-magic fa-fw"></i> <span class="js-action-text">Restore</span>',
display: topic_can_nuke_or_restore,
method: 'POST',
url: url('forum-topic-restore', { topic: topic_info.id }),
confirm: 'Are you sure you want to restore this topic?',
disableWith: 'Restoring...',
disableWithTarget: '.js-action-text',
withCsrf: true,
refreshOnSuccess: true,
},
{
'html': '<i class="fas fa-radiation-alt fa-fw"></i> Permanently Delete',
'url': url('forum-topic-nuke', { topic: topic_info.id, csrf: csrf_token() }),
'display': topic_can_nuke_or_restore,
html: '<i class="fas fa-radiation-alt fa-fw"></i> <span class="js-action-text">Permanently Delete</span>',
display: topic_can_nuke_or_restore,
method: 'POST',
url: url('forum-topic-nuke', { topic: topic_info.id }),
confirm: 'Are you sure you want to PERMANENTLY DELETE this topic?',
disableWith: 'Nuking...',
disableWithTarget: '.js-action-text',
withCsrf: true,
redirectOnSuccess: url('forum-category', { forum: category_info.id }),
},
{
'html': '<i class="fas fa-plus-circle fa-fw"></i> Bump',
'url': url('forum-topic-bump', { topic: topic_info.id, csrf: csrf_token() }),
'display': topic_can_bump,
html: '<i class="fas fa-arrow-alt-circle-up fa-fw"></i> <span class="js-action-text">Bump</span>',
display: topic_can_bump,
method: 'POST',
url: url('forum-topic-bump', { topic: topic_info.id }),
disableWith: 'Bumping...',
disableWithTarget: '.js-action-text',
withCsrf: true,
},
{
'html': '<i class="fas fa-lock fa-fw"></i> Lock',
'url': url('forum-topic-lock', { topic: topic_info.id, csrf: csrf_token() }),
'display': topic_can_lock and not topic_info.locked,
html: '<i class="fas fa-lock fa-fw"></i> <span class="js-action-text">Lock</span>',
display: topic_can_lock and not topic_info.locked,
method: 'POST',
url: url('forum-topic-lock', { topic: topic_info.id }),
disableWith: 'Locking...',
disableWithTarget: '.js-action-text',
withCsrf: true,
refreshOnSuccess: true,
},
{
'html': '<i class="fas fa-lock-open fa-fw"></i> Unlock',
'url': url('forum-topic-unlock', { topic: topic_info.id, csrf: csrf_token() }),
'display': topic_can_lock and topic_info.locked,
html: '<i class="fas fa-lock-open fa-fw"></i> <span class="js-action-text">Unlock</span>',
display: topic_can_lock and topic_info.locked,
method: 'POST',
url: url('forum-topic-unlock', { topic: topic_info.id }),
disableWith: 'Unlocking...',
disableWithTarget: '.js-action-text',
withCsrf: true,
refreshOnSuccess: true,
},
] %}