Compare commits
5 commits
Author | SHA1 | Date | |
---|---|---|---|
0633a48f09 | |||
ceb05fc3f7 | |||
7ea5e9414d | |||
419e9b3e0a | |||
cc72b8211b |
74 changed files with 3743 additions and 3156 deletions
assets/css/misuzu
database
misuzu.phppublic
src
Colour.phpMemoizer.phpPagination.phpTwigMisuzu.php
Console/Commands
Database
Forum
ForumCategory.phpForumException.phpForumLeaderboard.phpForumPoll.phpForumPollAnswer.phpForumPollOption.phpForumPost.phpForumTopic.phpForumTopicPriority.phpForumTopicTrack.phpForumTrack.phpforum.phpleaderboard.phpperms.phppoll.phppost.phptopic.phpvalidate.php
Http
Handlers
Forum
ForumCategoryHandler.phpForumHandler.phpForumIndexHandler.phpForumPollHandler.phpForumPostHandler.phpForumTopicHandler.php
ForumHandler.phpRouting
Users
url.phptemplates
utility.php
|
@ -3,7 +3,7 @@
|
|||
width: 100%;
|
||||
display: flex;
|
||||
border-width: 0;
|
||||
border-color: var(--text-colour);
|
||||
border-color: #000;
|
||||
border-style: solid;
|
||||
border-top-width: 1px;
|
||||
align-items: flex-start;
|
||||
|
@ -23,7 +23,7 @@
|
|||
.navigation__option {
|
||||
list-style: none;
|
||||
background-color: #c9bbcc;
|
||||
border: 1px solid var(--text-colour);
|
||||
border: 1px solid #000;
|
||||
border-top-width: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
@ -31,6 +31,7 @@
|
|||
.navigation__option--selected {
|
||||
background-color: var(--accent-colour);
|
||||
top: -1px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.navigation__option--selected:not(:first-child) {
|
||||
margin-left: -1px;
|
||||
|
@ -40,7 +41,7 @@
|
|||
.navigation__link {
|
||||
display: block;
|
||||
padding: 2px 1em;
|
||||
color: var(--text-colour);
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.navigation__link:hover, .navigation__link:focus { color: #609; }
|
||||
|
|
66
database/2020_06_20_194341_forum_updates.php
Normal file
66
database/2020_06_20_194341_forum_updates.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
namespace Misuzu\DatabaseMigrations\ForumUpdates;
|
||||
|
||||
use PDO;
|
||||
|
||||
function migrate_up(PDO $conn): void {
|
||||
$conn->exec("
|
||||
ALTER TABLE `msz_forum_topics`
|
||||
ADD COLUMN `topic_count_posts` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `topic_title`,
|
||||
ADD COLUMN `topic_post_first` INT(10) UNSIGNED NULL DEFAULT NULL AFTER `topic_count_views`,
|
||||
ADD COLUMN `topic_post_last` INT(10) UNSIGNED NULL DEFAULT NULL AFTER `topic_post_first`,
|
||||
DROP COLUMN `poll_id`,
|
||||
DROP INDEX `posts_poll_id_foreign`,
|
||||
DROP FOREIGN KEY `posts_poll_id_foreign`,
|
||||
ADD INDEX `topics_post_first_foreign` (`topic_post_first`),
|
||||
ADD INDEX `topics_post_last_foreign` (`topic_post_last`),
|
||||
ADD CONSTRAINT `topics_post_first_foreign`
|
||||
FOREIGN KEY (`topic_post_first`)
|
||||
REFERENCES `msz_forum_posts` (`post_id`)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE SET NULL,
|
||||
ADD CONSTRAINT `topics_post_last_foreign`
|
||||
FOREIGN KEY (`topic_post_last`)
|
||||
REFERENCES `msz_forum_posts` (`post_id`)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE SET NULL;
|
||||
");
|
||||
|
||||
$conn->exec("
|
||||
ALTER TABLE `msz_forum_polls`
|
||||
ADD COLUMN `topic_id` INT(10) UNSIGNED NULL DEFAULT NULL AFTER `poll_id`,
|
||||
ADD INDEX `forum_poll_topic_foreign` (`topic_id`),
|
||||
ADD CONSTRAINT `forum_poll_topic_foreign`
|
||||
FOREIGN KEY (`topic_id`)
|
||||
REFERENCES `msz_forum_topics` (`topic_id`)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE;
|
||||
");
|
||||
}
|
||||
|
||||
function migrate_down(PDO $conn): void {
|
||||
$conn->exec("
|
||||
ALTER TABLE `msz_forum_polls`
|
||||
DROP COLUMN `topic_id`,
|
||||
DROP INDEX `forum_poll_topic_foreign`,
|
||||
DROP FOREIGN KEY `forum_poll_topic_foreign`;
|
||||
");
|
||||
|
||||
$conn->exec("
|
||||
ALTER TABLE `msz_forum_topics`
|
||||
ADD COLUMN `poll_id` INT(10) UNSIGNED NULL DEFAULT NULL AFTER `user_id`,
|
||||
DROP COLUMN `topic_count_posts`,
|
||||
DROP COLUMN `topic_post_first`,
|
||||
DROP COLUMN `topic_post_last`,
|
||||
DROP INDEX `topics_post_first_foreign`,
|
||||
DROP INDEX `topics_post_last_foreign`,
|
||||
DROP FOREIGN KEY `topics_post_first_foreign`,
|
||||
DROP FOREIGN KEY `topics_post_last_foreign`,
|
||||
ADD INDEX `posts_poll_id_foreign` (`poll_id`),
|
||||
ADD CONSTRAINT `posts_poll_id_foreign`
|
||||
FOREIGN KEY (`poll_id`)
|
||||
REFERENCES `msz_users` (`poll_id`)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE SET NULL;
|
||||
");
|
||||
}
|
12
misuzu.php
12
misuzu.php
|
@ -76,12 +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/leaderboard.php';
|
||||
require_once 'src/Forum/poll.php';
|
||||
require_once 'src/Forum/post.php';
|
||||
require_once 'src/Forum/topic.php';
|
||||
require_once 'src/Forum/validate.php';
|
||||
|
||||
$dbConfig = parse_ini_file(MSZ_CONFIG . '/config.ini', true, INI_SCANNER_TYPED);
|
||||
|
||||
|
@ -123,12 +117,6 @@ if(MSZ_CLI) { // Temporary backwards compatibility measure, remove this later
|
|||
// Everything below here should eventually be moved to index.php, probably only initialised when required.
|
||||
// Serving things like the css/js doesn't need to initialise sessions.
|
||||
|
||||
if(!mb_check_encoding()) {
|
||||
http_response_code(415);
|
||||
echo 'Invalid request encoding.';
|
||||
exit;
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
if(file_exists(MSZ_ROOT . '/.migrating')) {
|
||||
|
|
|
@ -1,81 +1,2 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Users\User;
|
||||
|
||||
require_once '../../misuzu.php';
|
||||
|
||||
$forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0;
|
||||
$forumId = max($forumId, 0);
|
||||
|
||||
if($forumId === 0) {
|
||||
url_redirect('forum-index');
|
||||
exit;
|
||||
}
|
||||
|
||||
$forum = forum_get($forumId);
|
||||
$forumUser = User::getCurrent();
|
||||
$forumUserId = $forumUser === null ? 0 : $forumUser->getId();
|
||||
|
||||
if(empty($forum) || ($forum['forum_type'] == MSZ_FORUM_TYPE_LINK && empty($forum['forum_link']))) {
|
||||
echo render_error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$perms = forum_perms_get_user($forum['forum_id'], $forumUserId)[MSZ_FORUM_PERMS_GENERAL];
|
||||
|
||||
if(!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM)) {
|
||||
echo render_error(403);
|
||||
return;
|
||||
}
|
||||
|
||||
if(isset($forumUser) && $forumUser->hasActiveWarning())
|
||||
$perms &= ~MSZ_FORUM_PERM_SET_WRITE;
|
||||
|
||||
Template::set('forum_perms', $perms);
|
||||
|
||||
if($forum['forum_type'] == MSZ_FORUM_TYPE_LINK) {
|
||||
forum_increment_clicks($forum['forum_id']);
|
||||
redirect($forum['forum_link']);
|
||||
return;
|
||||
}
|
||||
|
||||
$forumPagination = new Pagination($forum['forum_topic_count'], 20);
|
||||
|
||||
if(!$forumPagination->hasValidOffset() && $forum['forum_topic_count'] > 0) {
|
||||
echo render_error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$forumMayHaveTopics = forum_may_have_topics($forum['forum_type']);
|
||||
$topics = $forumMayHaveTopics
|
||||
? forum_topic_listing(
|
||||
$forum['forum_id'],
|
||||
$forumUserId,
|
||||
$forumPagination->getOffset(),
|
||||
$forumPagination->getRange(),
|
||||
perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST),
|
||||
forum_has_priority_voting($forum['forum_type'])
|
||||
)
|
||||
: [];
|
||||
|
||||
$forumMayHaveChildren = forum_may_have_children($forum['forum_type']);
|
||||
|
||||
if($forumMayHaveChildren) {
|
||||
$forum['forum_subforums'] = forum_get_children($forum['forum_id'], $forumUserId);
|
||||
|
||||
foreach($forum['forum_subforums'] as $skey => $subforum) {
|
||||
$forum['forum_subforums'][$skey]['forum_subforums']
|
||||
= forum_get_children($subforum['forum_id'], $forumUserId);
|
||||
}
|
||||
}
|
||||
|
||||
Template::render('forum.forum', [
|
||||
'forum_breadcrumbs' => forum_get_breadcrumbs($forum['forum_id']),
|
||||
'global_accent_colour' => forum_get_colour($forum['forum_id']),
|
||||
'forum_may_have_topics' => $forumMayHaveTopics,
|
||||
'forum_may_have_children' => $forumMayHaveChildren,
|
||||
'forum_info' => $forum,
|
||||
'forum_topics' => $topics,
|
||||
'forum_pagination' => $forumPagination,
|
||||
]);
|
||||
require_once __DIR__ . '/../index.php';
|
||||
|
|
|
@ -1,41 +1,2 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Users\User;
|
||||
|
||||
require_once '../../misuzu.php';
|
||||
|
||||
$indexMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
|
||||
$forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0;
|
||||
|
||||
$currentUser = User::getCurrent();
|
||||
$currentUserId = $currentUser === null ? 0 : $currentUser->getId();
|
||||
|
||||
switch($indexMode) {
|
||||
case 'mark':
|
||||
url_redirect($forumId < 1 ? 'forum-mark-global' : 'forum-mark-single', ['forum' => $forumId]);
|
||||
break;
|
||||
|
||||
default:
|
||||
$categories = forum_get_root_categories($currentUserId);
|
||||
$blankForum = count($categories) < 1;
|
||||
|
||||
foreach($categories as $key => $category) {
|
||||
$categories[$key]['forum_subforums'] = forum_get_children($category['forum_id'], $currentUserId);
|
||||
|
||||
foreach($categories[$key]['forum_subforums'] as $skey => $sub) {
|
||||
if(!forum_may_have_children($sub['forum_type'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$categories[$key]['forum_subforums'][$skey]['forum_subforums']
|
||||
= forum_get_children($sub['forum_id'], $currentUserId);
|
||||
}
|
||||
}
|
||||
|
||||
Template::render('forum.index', [
|
||||
'forum_categories' => $categories,
|
||||
'forum_empty' => $blankForum,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
require_once __DIR__ . '/../index.php';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Forum\ForumLeaderboard;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
require_once '../../misuzu.php';
|
||||
|
@ -14,7 +15,7 @@ $leaderboardMode = !empty($_GET['mode']) && is_string($_GET['mode']) && ctype_lo
|
|||
$leaderboardId = !empty($_GET['id']) && is_string($_GET['id'])
|
||||
&& ctype_digit($_GET['id'])
|
||||
? $_GET['id']
|
||||
: MSZ_FORUM_LEADERBOARD_CATEGORY_ALL;
|
||||
: ForumLeaderboard::CATEGORY_ALL;
|
||||
$leaderboardIdLength = strlen($leaderboardId);
|
||||
|
||||
$leaderboardYear = $leaderboardIdLength === 4 || $leaderboardIdLength === 6 ? substr($leaderboardId, 0, 4) : null;
|
||||
|
@ -22,8 +23,8 @@ $leaderboardMonth = $leaderboardIdLength === 6 ? substr($leaderboardId, 4, 2) :
|
|||
|
||||
$unrankedForums = !empty($_GET['allow_unranked']) ? [] : Config::get('forum_leader.unranked.forum', Config::TYPE_ARR);
|
||||
$unrankedTopics = !empty($_GET['allow_unranked']) ? [] : Config::get('forum_leader.unranked.topic', Config::TYPE_ARR);
|
||||
$leaderboards = forum_leaderboard_categories();
|
||||
$leaderboard = forum_leaderboard_listing($leaderboardYear, $leaderboardMonth, $unrankedForums, $unrankedTopics);
|
||||
$leaderboards = ForumLeaderboard::categories();
|
||||
$leaderboard = ForumLeaderboard::listing($leaderboardYear, $leaderboardMonth, $unrankedForums, $unrankedTopics);
|
||||
|
||||
$leaderboardName = 'All Time';
|
||||
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Users\User;
|
||||
|
||||
require_once '../../misuzu.php';
|
||||
|
||||
$redirect = !empty($_SERVER['HTTP_REFERER']) && empty($_SERVER['HTTP_X_MISUZU_XHR']) ? $_SERVER['HTTP_REFERER'] : '';
|
||||
$isXHR = !$redirect;
|
||||
|
||||
if($isXHR) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
} elseif(!is_local_url($redirect)) {
|
||||
echo render_info('Possible request forgery detected.', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!CSRF::validateRequest()) {
|
||||
echo render_info_or_json($isXHR, "Couldn't verify this request, please refresh the page and try again.", 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUser = User::getCurrent();
|
||||
|
||||
if($currentUser === null) {
|
||||
echo render_info_or_json($isXHR, 'You must be logged in to vote on polls.', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = $currentUser->getId();
|
||||
|
||||
if($currentUser->isBanned()) {
|
||||
echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403);
|
||||
return;
|
||||
}
|
||||
if($currentUser->isSilenced()) {
|
||||
echo render_info_or_json($isXHR, 'You have been silenced, check your profile for more information.', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
header(CSRF::header());
|
||||
|
||||
if(empty($_POST['poll']['id']) || !ctype_digit($_POST['poll']['id'])) {
|
||||
echo render_info_or_json($isXHR, "Invalid request.", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$poll = forum_poll_get($_POST['poll']['id']);
|
||||
|
||||
if(empty($poll)) {
|
||||
echo "Poll {$poll['poll_id']} doesn't exist.<br>";
|
||||
return;
|
||||
}
|
||||
|
||||
$topicInfo = forum_poll_get_topic($poll['poll_id']);
|
||||
|
||||
if(!is_null($topicInfo['topic_locked'])) {
|
||||
echo "The topic associated with this poll has been locked.<br>";
|
||||
return;
|
||||
}
|
||||
|
||||
if(!forum_perms_check_user(
|
||||
MSZ_FORUM_PERMS_GENERAL, $topicInfo['forum_id'],
|
||||
$currentUserId, MSZ_FORUM_PERM_SET_READ
|
||||
)) {
|
||||
echo "You aren't allowed to vote on this poll.<br>";
|
||||
return;
|
||||
}
|
||||
|
||||
if($poll['poll_expired']) {
|
||||
echo "Voting for poll {$poll['poll_id']} has closed.<br>";
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$poll['poll_change_vote'] && forum_poll_has_voted($currentUserId, $poll['poll_id'])) {
|
||||
echo "Can't change vote for {$poll['poll_id']}<br>";
|
||||
return;
|
||||
}
|
||||
|
||||
$answers = !empty($_POST['poll']['answers'])
|
||||
&& is_array($_POST['poll']['answers'])
|
||||
? $_POST['poll']['answers']
|
||||
: [];
|
||||
|
||||
if(count($answers) > $poll['poll_max_votes']) {
|
||||
echo "Too many votes for poll {$poll['poll_id']}<br>";
|
||||
return;
|
||||
}
|
||||
|
||||
forum_poll_vote_remove($currentUserId, $poll['poll_id']);
|
||||
|
||||
foreach($answers as $answerId) {
|
||||
if(!is_string($answerId) || !ctype_digit($answerId)
|
||||
|| !forum_poll_validate_option($poll['poll_id'], (int)$answerId)) {
|
||||
echo "Vote {$answerId} was invalid for {$poll['poll_id']}<br>";
|
||||
continue;
|
||||
}
|
||||
|
||||
forum_poll_vote_cast($currentUserId, $poll['poll_id'], (int)$answerId);
|
||||
}
|
||||
|
||||
url_redirect('forum-topic', ['topic' => $topicInfo['topic_id']]);
|
|
@ -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;
|
||||
|
||||
|
@ -30,7 +32,7 @@ if(!empty($postMode) && !UserSession::hasCurrent()) {
|
|||
return;
|
||||
}
|
||||
|
||||
$currentUser = User::getCurrent():
|
||||
$currentUser = User::getCurrent();
|
||||
$currentUserId = $currentUser === null ? 0 : $currentUser->getId();
|
||||
|
||||
if(isset($currentUser) && $currentUser->isBanned()) {
|
||||
|
@ -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'] / MSZ_FORUM_POSTS_PER_PAGE) + 1,
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Forum\ForumCategory;
|
||||
use Misuzu\Forum\ForumCategoryNotFoundException;
|
||||
use Misuzu\Forum\ForumTopic;
|
||||
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;
|
||||
use Misuzu\Users\User;
|
||||
|
@ -63,42 +73,37 @@ if(empty($postId) && empty($topicId) && empty($forumId)) {
|
|||
return;
|
||||
}
|
||||
|
||||
if(!empty($postId)) {
|
||||
$post = forum_post_get($postId);
|
||||
if(!empty($postId))
|
||||
try {
|
||||
$postInfo = ForumPost::byId($postId);
|
||||
$topicId = $postInfo->getTopicId();
|
||||
} catch(ForumPostNotFoundException $ex) {}
|
||||
|
||||
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($topicId))
|
||||
try {
|
||||
$topicInfo = ForumTopic::byId($topicId);
|
||||
$forumId = $topicInfo->getCategoryId();
|
||||
} catch(ForumTopicNotFoundException $ex) {}
|
||||
|
||||
if(!empty($topicId)) {
|
||||
$topic = forum_topic_get($topicId);
|
||||
|
||||
if(isset($topic['forum_id'])) {
|
||||
$forumId = (int)$topic['forum_id'];
|
||||
}
|
||||
}
|
||||
|
||||
if(!empty($forumId)) {
|
||||
$forum = forum_get($forumId);
|
||||
}
|
||||
|
||||
if(empty($forum)) {
|
||||
try {
|
||||
$forumInfo = ForumCategory::byId($forumId);
|
||||
} catch(ForumCategoryNotFoundException $ex) {
|
||||
echo render_error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$perms = forum_perms_get_user($forum['forum_id'], $currentUserId)[MSZ_FORUM_PERMS_GENERAL];
|
||||
$perms = forum_perms_get_user($forumInfo->getId(), $currentUserId)[MSZ_FORUM_PERMS_GENERAL];
|
||||
|
||||
if($forum['forum_archived']
|
||||
|| (!empty($topic['topic_locked']) && !perms_check($perms, MSZ_FORUM_PERM_LOCK_TOPIC))
|
||||
if($forumInfo->isArchived()
|
||||
|| (!empty($topicInfo) && $topicInfo->isLocked() && !perms_check($perms, MSZ_FORUM_PERM_LOCK_TOPIC))
|
||||
|| !perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM | MSZ_FORUM_PERM_CREATE_POST)
|
||||
|| (empty($topic) && !perms_check($perms, MSZ_FORUM_PERM_CREATE_TOPIC))) {
|
||||
|| (empty($topicInfo) && !perms_check($perms, MSZ_FORUM_PERM_CREATE_TOPIC))) {
|
||||
echo render_error(403);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!forum_may_have_topics($forum['forum_type'])) {
|
||||
if(!$forumInfo->canHaveTopics()) {
|
||||
echo render_error(400);
|
||||
return;
|
||||
}
|
||||
|
@ -106,48 +111,45 @@ if(!forum_may_have_topics($forum['forum_type'])) {
|
|||
$topicTypes = [];
|
||||
|
||||
if($mode === 'create' || $mode === 'edit') {
|
||||
$topicTypes[MSZ_TOPIC_TYPE_DISCUSSION] = 'Normal discussion';
|
||||
|
||||
if(perms_check($perms, MSZ_FORUM_PERM_STICKY_TOPIC)) {
|
||||
$topicTypes[MSZ_TOPIC_TYPE_STICKY] = 'Sticky topic';
|
||||
}
|
||||
if(perms_check($perms, MSZ_FORUM_PERM_ANNOUNCE_TOPIC)) {
|
||||
$topicTypes[MSZ_TOPIC_TYPE_ANNOUNCEMENT] = 'Announcement';
|
||||
}
|
||||
if(perms_check($perms, MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC)) {
|
||||
$topicTypes[MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT] = 'Global Announcement';
|
||||
}
|
||||
$topicTypes[ForumTopic::TYPE_DISCUSSION] = 'Normal discussion';
|
||||
if(perms_check($perms, MSZ_FORUM_PERM_STICKY_TOPIC))
|
||||
$topicTypes[ForumTopic::TYPE_STICKY] = 'Sticky topic';
|
||||
if(perms_check($perms, MSZ_FORUM_PERM_ANNOUNCE_TOPIC))
|
||||
$topicTypes[ForumTopic::TYPE_ANNOUNCEMENT] = 'Announcement';
|
||||
if(perms_check($perms, MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC))
|
||||
$topicTypes[ForumTopic::TYPE_GLOBAL_ANNOUNCEMENT] = 'Global Announcement';
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
$notices = [];
|
||||
$isNewTopic = false;
|
||||
|
||||
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 = empty($topic) || ($mode === 'edit' && $post['is_opening_post']);
|
||||
$isEditingTopic = $isNewTopic || ($mode === 'edit' && $postInfo->isOpeningPost());
|
||||
|
||||
if($mode === 'create') {
|
||||
$timeoutCheck = max(1, forum_timeout($forumId, $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));
|
||||
|
@ -156,20 +158,14 @@ if(!empty($_POST)) {
|
|||
}
|
||||
|
||||
if($isEditingTopic) {
|
||||
$originalTopicTitle = $topic['topic_title'] ?? null;
|
||||
$originalTopicTitle = $isNewTopic ? null : $topicInfo->getTitle();
|
||||
$topicTitleChanged = $topicTitle !== $originalTopicTitle;
|
||||
$originalTopicType = (int)($topic['topic_type'] ?? MSZ_TOPIC_TYPE_DISCUSSION);
|
||||
$originalTopicType = $isNewTopic ? ForumTopic::TYPE_DISCUSSION : $topicInfo->getType();
|
||||
$topicTypeChanged = $topicType !== null && $topicType !== $originalTopicType;
|
||||
|
||||
switch(forum_validate_title($topicTitle)) {
|
||||
case 'too-short':
|
||||
$notices[] = 'Topic title was too short.';
|
||||
break;
|
||||
|
||||
case 'too-long':
|
||||
$notices[] = 'Topic title was too long.';
|
||||
break;
|
||||
}
|
||||
$validateTopicTitle = ForumTopic::validateTitle($topicTitle);
|
||||
if(!empty($validateTopicTitle))
|
||||
$notices[] = ForumTopic::titleValidationErrorString($validateTopicTitle);
|
||||
|
||||
if($mode === 'create' && $topicType === null) {
|
||||
$topicType = array_key_first($topicTypes);
|
||||
|
@ -178,54 +174,52 @@ if(!empty($_POST)) {
|
|||
}
|
||||
}
|
||||
|
||||
if(!Parser::isValid($postParser)) {
|
||||
if(!Parser::isValid($postParser))
|
||||
$notices[] = 'Invalid parser selected.';
|
||||
}
|
||||
|
||||
switch(forum_validate_post($postText)) {
|
||||
case 'too-short':
|
||||
$notices[] = 'Post content was too short.';
|
||||
break;
|
||||
|
||||
case 'too-long':
|
||||
$notices[] = 'Post content was too long.';
|
||||
break;
|
||||
}
|
||||
$postBodyValidation = ForumPost::validateBody($postText);
|
||||
if(!empty($postBodyValidation))
|
||||
$notices[] = ForumPost::bodyValidationErrorString($postBodyValidation);
|
||||
|
||||
if(empty($notices)) {
|
||||
switch($mode) {
|
||||
case 'create':
|
||||
if(!empty($topic)) {
|
||||
forum_topic_bump($topic['topic_id']);
|
||||
if(!empty($topicInfo)) {
|
||||
$topicInfo->bumpTopic();
|
||||
} else {
|
||||
$topicId = forum_topic_create(
|
||||
$forum['forum_id'],
|
||||
$currentUserId,
|
||||
$topicTitle,
|
||||
$topicType
|
||||
);
|
||||
$isNewTopic = true;
|
||||
$topicInfo = ForumTopic::create($forumInfo, $currentUser, $topicTitle, $topicType);
|
||||
$topicId = $topicInfo->getId();
|
||||
}
|
||||
|
||||
$postId = forum_post_create(
|
||||
$topicId,
|
||||
$forum['forum_id'],
|
||||
$currentUserId,
|
||||
IPAddress::remote(),
|
||||
$postText,
|
||||
$postParser,
|
||||
$postSignature
|
||||
);
|
||||
forum_topic_mark_read($currentUserId, $topicId, $forum['forum_id']);
|
||||
forum_count_increase($forum['forum_id'], empty($topic));
|
||||
$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.';
|
||||
}
|
||||
|
||||
if($isEditingTopic && ($topicTitleChanged || $topicTypeChanged)) {
|
||||
if(!forum_topic_update($topicId, $topicTitle, $topicType)) {
|
||||
$topicInfo->setTitle($topicTitle)->setType($topicType);
|
||||
|
||||
try {
|
||||
$topicInfo->update();
|
||||
} catch(ForumTopicUpdateFailedException $ex) {
|
||||
$notices[] = 'Topic update failed.';
|
||||
}
|
||||
}
|
||||
|
@ -233,7 +227,7 @@ if(!empty($_POST)) {
|
|||
}
|
||||
|
||||
if(empty($notices)) {
|
||||
$redirect = url(empty($topic) ? 'forum-topic' : 'forum-post', [
|
||||
$redirect = url($isNewTopic ? 'forum-topic' : 'forum-post', [
|
||||
'topic' => $topicId ?? 0,
|
||||
'post' => $postId ?? 0,
|
||||
'post_fragment' => 'p' . ($postId ?? 0),
|
||||
|
@ -245,21 +239,18 @@ if(!empty($_POST)) {
|
|||
}
|
||||
}
|
||||
|
||||
if(!empty($topic)) {
|
||||
Template::set('posting_topic', $topic);
|
||||
if(!$isNewTopic && !empty($topicInfo)) {
|
||||
Template::set('posting_topic', $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);
|
||||
}
|
||||
|
||||
$displayInfo = forum_posting_info($currentUserId);
|
||||
|
||||
Template::render('forum.posting', [
|
||||
'posting_breadcrumbs' => forum_get_breadcrumbs($forumId),
|
||||
'global_accent_colour' => forum_get_colour($forumId),
|
||||
'posting_forum' => $forum,
|
||||
'posting_info' => $displayInfo,
|
||||
'global_accent_colour' => $forumInfo->getColour(),
|
||||
'posting_forum' => $forumInfo,
|
||||
'posting_user' => $currentUser,
|
||||
'posting_notices' => $notices,
|
||||
'posting_mode' => $mode,
|
||||
'posting_types' => $topicTypes,
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserNotFoundException;
|
||||
|
||||
require_once '../../misuzu.php';
|
||||
|
||||
if(!MSZ_DEBUG)
|
||||
return;
|
||||
|
||||
$topicId = !empty($_GET['t']) && is_string($_GET['t']) ? (int)$_GET['t'] : 0;
|
||||
$topicUser = User::getCurrent();
|
||||
$topicUserId = $topicUser === null ? 0 : $topicUser->getId();
|
||||
|
||||
if($topicUserId < 1) {
|
||||
echo render_error(403);
|
||||
return;
|
||||
}
|
||||
|
||||
$topic = forum_topic_get($topicId, true);
|
||||
$perms = $topic
|
||||
? forum_perms_get_user($topic['forum_id'], $topicUserId)[MSZ_FORUM_PERMS_GENERAL]
|
||||
: 0;
|
||||
|
||||
if(isset($topicUser) && $topicUser->hasActiveWarning())
|
||||
$perms &= ~MSZ_FORUM_PERM_SET_WRITE;
|
||||
|
||||
$topicIsDeleted = !empty($topic['topic_deleted']);
|
||||
$canDeleteAny = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST);
|
||||
|
||||
if(!$topic || ($topicIsDeleted && !$canDeleteAny)) {
|
||||
echo render_error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM, true) // | MSZ_FORUM_PERM_PRIORITY_VOTE
|
||||
|| !$canDeleteAny
|
||||
&& (
|
||||
!empty($topic['topic_locked'])
|
||||
|| !empty($topic['topic_archived'])
|
||||
)
|
||||
) {
|
||||
echo render_error(403);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!forum_has_priority_voting($topic['forum_type'])) {
|
||||
echo render_error(400);
|
||||
return;
|
||||
}
|
||||
|
||||
forum_topic_priority_increase($topicId, $topicUserId);
|
||||
|
||||
url_redirect('forum-topic', ['topic' => $topicId]);
|
|
@ -2,39 +2,47 @@
|
|||
namespace Misuzu;
|
||||
|
||||
use Misuzu\AuditLog;
|
||||
use Misuzu\Forum\ForumTopic;
|
||||
use Misuzu\Forum\ForumTopicNotFoundException;
|
||||
use Misuzu\Forum\ForumPost;
|
||||
use Misuzu\Forum\ForumPostNotFoundException;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
require_once '../../misuzu.php';
|
||||
|
||||
$postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
|
||||
$topicId = !empty($_GET['t']) && is_string($_GET['t']) ? (int)$_GET['t'] : 0;
|
||||
$moderationMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
|
||||
$submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1';
|
||||
$postId = (int)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT);
|
||||
$topicId = (int)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT);
|
||||
$moderationMode = (string)filter_input(INPUT_GET, 'm', FILTER_SANITIZE_STRING);
|
||||
$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);
|
||||
} catch(ForumTopicNotFoundException $ex) {
|
||||
echo render_error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$topic = forum_topic_get($topicId, true);
|
||||
$perms = $topic
|
||||
? forum_perms_get_user($topic['forum_id'], $topicUserId)[MSZ_FORUM_PERMS_GENERAL]
|
||||
: 0;
|
||||
$perms = forum_perms_get_user($topicInfo->getCategory()->getId(), $topicUserId)[MSZ_FORUM_PERMS_GENERAL];
|
||||
|
||||
if(isset($topicUser) && $topicUser->hasActiveWarning())
|
||||
$perms &= ~MSZ_FORUM_PERM_SET_WRITE;
|
||||
|
||||
$topicIsDeleted = !empty($topic['topic_deleted']);
|
||||
$canDeleteAny = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST);
|
||||
|
||||
if(!$topic || ($topicIsDeleted && !$canDeleteAny)) {
|
||||
if($topicInfo->isDeleted() && !$canDeleteAny) {
|
||||
echo render_error(404);
|
||||
return;
|
||||
}
|
||||
|
@ -44,29 +52,18 @@ if(!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM)) {
|
|||
return;
|
||||
}
|
||||
|
||||
if(!empty($topic['poll_id'])) {
|
||||
$pollOptions = forum_poll_get_options($topic['poll_id']);
|
||||
$pollUserAnswers = forum_poll_get_user_answers($topic['poll_id'], $topicUserId);
|
||||
}
|
||||
|
||||
if(forum_has_priority_voting($topic['forum_type'])) {
|
||||
$topicPriority = forum_topic_priority($topic['topic_id']);
|
||||
}
|
||||
|
||||
$topicIsLocked = !empty($topic['topic_locked']);
|
||||
$topicIsArchived = !empty($topic['topic_archived']);
|
||||
$topicPostsTotal = (int)($topic['topic_count_posts'] + $topic['topic_count_posts_deleted']);
|
||||
$topicIsFrozen = $topicIsArchived || $topicIsDeleted;
|
||||
$canDeleteOwn = !$topicIsFrozen && !$topicIsLocked && perms_check($perms, MSZ_FORUM_PERM_DELETE_POST);
|
||||
$topicPostsTotal = $topicInfo->getActualPostCount(true);
|
||||
$topicIsFrozen = $topicInfo->isArchived() || $topicInfo->isDeleted();
|
||||
$canDeleteOwn = !$topicIsFrozen && !$topicInfo->isLocked() && perms_check($perms, MSZ_FORUM_PERM_DELETE_POST);
|
||||
$canBumpTopic = !$topicIsFrozen && perms_check($perms, MSZ_FORUM_PERM_BUMP_TOPIC);
|
||||
$canLockTopic = !$topicIsFrozen && perms_check($perms, MSZ_FORUM_PERM_LOCK_TOPIC);
|
||||
$canNukeOrRestore = $canDeleteAny && $topicIsDeleted;
|
||||
$canDelete = !$topicIsDeleted && (
|
||||
$canNukeOrRestore = $canDeleteAny && $topicInfo->isDeleted();
|
||||
$canDelete = !$topicInfo->isDeleted() && (
|
||||
$canDeleteAny || (
|
||||
$topicPostsTotal > 0
|
||||
&& $topicPostsTotal <= MSZ_FORUM_TOPIC_DELETE_POST_LIMIT
|
||||
&& $topicPostsTotal <= ForumTopic::DELETE_POST_LIMIT
|
||||
&& $canDeleteOwn
|
||||
&& $topic['author_user_id'] === $topicUserId
|
||||
&& $topicInfo->getUserId() === $topicUserId
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -109,52 +106,25 @@ if(in_array($moderationMode, $validModerationModes, true)) {
|
|||
|
||||
switch($moderationMode) {
|
||||
case 'delete':
|
||||
$canDeleteCode = forum_topic_can_delete($topic, $topicUserId);
|
||||
$canDeleteMsg = '';
|
||||
$responseCode = 200;
|
||||
$canDeleteCodes = [
|
||||
'view' => 404,
|
||||
'deleted' => 404,
|
||||
'owner' => 403,
|
||||
'age' => 403,
|
||||
'permission' => 403,
|
||||
'posts' => 403,
|
||||
'' => 200,
|
||||
];
|
||||
$canDelete = $topicInfo->canBeDeleted($topicUser);
|
||||
$canDeleteMsg = ForumTopic::canBeDeletedErrorString($canDelete);
|
||||
$responseCode = $canDeleteCodes[$canDelete] ?? 500;
|
||||
|
||||
switch($canDeleteCode) {
|
||||
case MSZ_E_FORUM_TOPIC_DELETE_USER:
|
||||
$responseCode = 401;
|
||||
$canDeleteMsg = 'You must be logged in to delete topics.';
|
||||
break;
|
||||
case MSZ_E_FORUM_TOPIC_DELETE_TOPIC:
|
||||
$responseCode = 404;
|
||||
$canDeleteMsg = "This topic doesn't exist.";
|
||||
break;
|
||||
case MSZ_E_FORUM_TOPIC_DELETE_DELETED:
|
||||
$responseCode = 404;
|
||||
$canDeleteMsg = 'This topic has already been marked as deleted.';
|
||||
break;
|
||||
case MSZ_E_FORUM_TOPIC_DELETE_OWNER:
|
||||
$responseCode = 403;
|
||||
$canDeleteMsg = 'You can only delete your own topics.';
|
||||
break;
|
||||
case MSZ_E_FORUM_TOPIC_DELETE_OLD:
|
||||
$responseCode = 401;
|
||||
$canDeleteMsg = 'This topic has existed for too long. Ask a moderator to remove if it absolutely necessary.';
|
||||
break;
|
||||
case MSZ_E_FORUM_TOPIC_DELETE_PERM:
|
||||
$responseCode = 401;
|
||||
$canDeleteMsg = 'You are not allowed to delete topics.';
|
||||
break;
|
||||
case MSZ_E_FORUM_TOPIC_DELETE_POSTS:
|
||||
$responseCode = 403;
|
||||
$canDeleteMsg = 'This topic already has replies, you may no longer delete it. Ask a moderator to remove if it absolutely necessary.';
|
||||
break;
|
||||
case MSZ_E_FORUM_TOPIC_DELETE_OK:
|
||||
break;
|
||||
default:
|
||||
$responseCode = 500;
|
||||
$canDeleteMsg = sprintf('Unknown error \'%d\'', $canDelete);
|
||||
}
|
||||
|
||||
if($canDeleteCode !== MSZ_E_FORUM_TOPIC_DELETE_OK) {
|
||||
if($canDelete !== '') {
|
||||
if($isXHR) {
|
||||
http_response_code($responseCode);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'topic_id' => $topic['topic_id'],
|
||||
'topic_id' => $topicInfo->getId(),
|
||||
'code' => $canDeleteCode,
|
||||
'message' => $canDeleteMsg,
|
||||
]);
|
||||
|
@ -170,9 +140,9 @@ if(in_array($moderationMode, $validModerationModes, true)) {
|
|||
Template::render('forum.confirm', [
|
||||
'title' => 'Confirm topic deletion',
|
||||
'class' => 'far fa-trash-alt',
|
||||
'message' => sprintf('You are about to delete topic #%d. Are you sure about that?', $topic['topic_id']),
|
||||
'message' => sprintf('You are about to delete topic #%d. Are you sure about that?', $topicInfo->getId()),
|
||||
'params' => [
|
||||
't' => $topic['topic_id'],
|
||||
't' => $topicInfo->getId(),
|
||||
'm' => 'delete',
|
||||
],
|
||||
]);
|
||||
|
@ -180,34 +150,26 @@ if(in_array($moderationMode, $validModerationModes, true)) {
|
|||
} elseif(!$submissionConfirmed) {
|
||||
url_redirect(
|
||||
'forum-topic',
|
||||
['topic' => $topic['topic_id']]
|
||||
['topic' => $topicInfo->getId()]
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$deleteTopic = forum_topic_delete($topic['topic_id']);
|
||||
|
||||
if($deleteTopic) {
|
||||
AuditLog::create(AuditLog::FORUM_TOPIC_DELETE, [$topic['topic_id']]);
|
||||
}
|
||||
$topicInfo->delete();
|
||||
AuditLog::create(AuditLog::FORUM_TOPIC_DELETE, [$topicInfo->getId()]);
|
||||
|
||||
if($isXHR) {
|
||||
echo json_encode([
|
||||
'success' => $deleteTopic,
|
||||
'topic_id' => $topic['topic_id'],
|
||||
'message' => $deleteTopic ? 'Topic deleted!' : 'Failed to delete topic.',
|
||||
'success' => true,
|
||||
'topic_id' => $topicInfo->getId(),
|
||||
'message' => 'Topic deleted!',
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
if(!$deleteTopic) {
|
||||
echo render_error(500);
|
||||
break;
|
||||
}
|
||||
|
||||
url_redirect('forum-category', [
|
||||
'forum' => $topic['forum_id'],
|
||||
'forum' => $topicInfo->getCategoryId(),
|
||||
]);
|
||||
break;
|
||||
|
||||
|
@ -222,34 +184,28 @@ if(in_array($moderationMode, $validModerationModes, true)) {
|
|||
Template::render('forum.confirm', [
|
||||
'title' => 'Confirm topic restore',
|
||||
'class' => 'fas fa-magic',
|
||||
'message' => sprintf('You are about to restore topic #%d. Are you sure about that?', $topic['topic_id']),
|
||||
'message' => sprintf('You are about to restore topic #%d. Are you sure about that?', $topicInfo->getId()),
|
||||
'params' => [
|
||||
't' => $topic['topic_id'],
|
||||
't' => $topicInfo->getId(),
|
||||
'm' => 'restore',
|
||||
],
|
||||
]);
|
||||
break;
|
||||
} elseif(!$submissionConfirmed) {
|
||||
url_redirect('forum-topic', [
|
||||
'topic' => $topic['topic_id'],
|
||||
'topic' => $topicInfo->getId(),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$restoreTopic = forum_topic_restore($topic['topic_id']);
|
||||
|
||||
if(!$restoreTopic) {
|
||||
echo render_error(500);
|
||||
break;
|
||||
}
|
||||
|
||||
AuditLog::create(AuditLog::FORUM_TOPIC_RESTORE, [$topic['topic_id']]);
|
||||
$topicInfo->restore();
|
||||
AuditLog::create(AuditLog::FORUM_TOPIC_RESTORE, [$topicInfo->getId()]);
|
||||
http_response_code(204);
|
||||
|
||||
if(!$isXHR) {
|
||||
url_redirect('forum-category', [
|
||||
'forum' => $topic['forum_id'],
|
||||
'forum' => $topicInfo->getCategoryId(),
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
@ -265,124 +221,90 @@ if(in_array($moderationMode, $validModerationModes, true)) {
|
|||
Template::render('forum.confirm', [
|
||||
'title' => 'Confirm topic nuke',
|
||||
'class' => 'fas fa-radiation',
|
||||
'message' => sprintf('You are about to PERMANENTLY DELETE topic #%d. Are you sure about that?', $topic['topic_id']),
|
||||
'message' => sprintf('You are about to PERMANENTLY DELETE topic #%d. Are you sure about that?', $topicInfo->getId()),
|
||||
'params' => [
|
||||
't' => $topic['topic_id'],
|
||||
't' => $topicInfo->getId(),
|
||||
'm' => 'nuke',
|
||||
],
|
||||
]);
|
||||
break;
|
||||
} elseif(!$submissionConfirmed) {
|
||||
url_redirect('forum-topic', [
|
||||
'topic' => $topic['topic_id'],
|
||||
'topic' => $topicInfo->getId(),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$nukeTopic = forum_topic_nuke($topic['topic_id']);
|
||||
|
||||
if(!$nukeTopic) {
|
||||
echo render_error(500);
|
||||
break;
|
||||
}
|
||||
|
||||
AuditLog::create(AuditLog::FORUM_TOPIC_NUKE, [$topic['topic_id']]);
|
||||
$topicInfo->nuke();
|
||||
AuditLog::create(AuditLog::FORUM_TOPIC_NUKE, [$topicInfo->getId()]);
|
||||
http_response_code(204);
|
||||
|
||||
if(!$isXHR) {
|
||||
url_redirect('forum-category', [
|
||||
'forum' => $topic['forum_id'],
|
||||
'forum' => $topicInfo->getCategoryId(),
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bump':
|
||||
if($canBumpTopic && forum_topic_bump($topic['topic_id'])) {
|
||||
AuditLog::create(AuditLog::FORUM_TOPIC_BUMP, [$topic['topic_id']]);
|
||||
if($canBumpTopic) {
|
||||
$topicInfo->bumpTopic();
|
||||
AuditLog::create(AuditLog::FORUM_TOPIC_BUMP, [$topicInfo->getId()]);
|
||||
}
|
||||
|
||||
url_redirect('forum-topic', [
|
||||
'topic' => $topic['topic_id'],
|
||||
'topic' => $topicInfo->getId(),
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'lock':
|
||||
if($canLockTopic && !$topicIsLocked && forum_topic_lock($topic['topic_id'])) {
|
||||
AuditLog::create(AuditLog::FORUM_TOPIC_LOCK, [$topic['topic_id']]);
|
||||
if($canLockTopic && !$topicInfo->isLocked()) {
|
||||
$topicInfo->setLocked(true);
|
||||
AuditLog::create(AuditLog::FORUM_TOPIC_LOCK, [$topicInfo->getId()]);
|
||||
}
|
||||
|
||||
url_redirect('forum-topic', [
|
||||
'topic' => $topic['topic_id'],
|
||||
'topic' => $topicInfo->getId(),
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'unlock':
|
||||
if($canLockTopic && $topicIsLocked && forum_topic_unlock($topic['topic_id'])) {
|
||||
AuditLog::create(AuditLog::FORUM_TOPIC_UNLOCK, [$topic['topic_id']]);
|
||||
if($canLockTopic && $topicInfo->isLocked()) {
|
||||
$topicInfo->setLocked(false);
|
||||
AuditLog::create(AuditLog::FORUM_TOPIC_UNLOCK, [$topicInfo->getId()]);
|
||||
}
|
||||
|
||||
url_redirect('forum-topic', [
|
||||
'topic' => $topic['topic_id'],
|
||||
'topic' => $topicInfo->getId(),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$topicPosts = $topic['topic_count_posts'];
|
||||
$topicPagination = new Pagination($topicInfo->getActualPostCount($canDeleteAny), \Misuzu\Forum\ForumPost::PER_PAGE, 'page');
|
||||
|
||||
if($canDeleteAny) {
|
||||
$topicPosts += $topic['topic_count_posts_deleted'];
|
||||
}
|
||||
|
||||
$topicPagination = new Pagination($topicPosts, MSZ_FORUM_POSTS_PER_PAGE, 'page');
|
||||
|
||||
if(isset($postInfo['preceeding_post_count'])) {
|
||||
$preceedingPosts = $postInfo['preceeding_post_count'];
|
||||
|
||||
if($canDeleteAny) {
|
||||
$preceedingPosts += $postInfo['preceeding_post_deleted_count'];
|
||||
}
|
||||
|
||||
$topicPagination->setPage(floor($preceedingPosts / $topicPagination->getRange()), true);
|
||||
}
|
||||
if(isset($postInfo))
|
||||
$topicPagination->setPage($postInfo->getTopicPage($canDeleteAny, $topicPagination->getRange()));
|
||||
|
||||
if(!$topicPagination->hasValidOffset()) {
|
||||
echo render_error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
Template::set('topic_perms', $perms);
|
||||
$canReply = !$topicInfo->isArchived() && !$topicInfo->isLocked() && !$topicInfo->isDeleted() && perms_check($perms, MSZ_FORUM_PERM_CREATE_POST);
|
||||
|
||||
$posts = forum_post_listing(
|
||||
$topic['topic_id'],
|
||||
$topicPagination->getOffset(),
|
||||
$topicPagination->getRange(),
|
||||
perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST)
|
||||
);
|
||||
|
||||
if(!$posts) {
|
||||
echo render_error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$canReply = !$topicIsArchived && !$topicIsLocked && !$topicIsDeleted && perms_check($perms, MSZ_FORUM_PERM_CREATE_POST);
|
||||
|
||||
forum_topic_mark_read($topicUserId, $topic['topic_id'], $topic['forum_id']);
|
||||
$topicInfo->markRead($topicUser);
|
||||
|
||||
Template::render('forum.topic', [
|
||||
'topic_breadcrumbs' => forum_get_breadcrumbs($topic['forum_id']),
|
||||
'global_accent_colour' => forum_get_colour($topic['forum_id']),
|
||||
'topic_info' => $topic,
|
||||
'topic_posts' => $posts,
|
||||
'topic_perms' => $perms,
|
||||
'topic_info' => $topicInfo,
|
||||
'can_reply' => $canReply,
|
||||
'topic_pagination' => $topicPagination,
|
||||
'topic_can_delete' => $canDelete,
|
||||
'topic_can_view_deleted' => $canDeleteAny,
|
||||
'topic_can_nuke_or_restore' => $canNukeOrRestore,
|
||||
'topic_can_bump' => $canBumpTopic,
|
||||
'topic_can_lock' => $canLockTopic,
|
||||
'topic_poll_options' => $pollOptions ?? [],
|
||||
'topic_poll_user_answers' => $pollUserAnswers ?? [],
|
||||
'topic_priority_votes' => $topicPriority ?? [],
|
||||
]);
|
||||
|
|
152
public/index.php
152
public/index.php
|
@ -11,72 +11,106 @@ $request = HttpRequestMessage::fromGlobals();
|
|||
|
||||
Router::setHandlerFormat('\Misuzu\Http\Handlers\%sHandler');
|
||||
Router::setFilterFormat('\Misuzu\Http\Filters\%sFilter');
|
||||
Router::addRoutes(
|
||||
// Home
|
||||
Route::get('/', 'index', 'Home'),
|
||||
|
||||
// Assets
|
||||
Route::group('/assets', 'Assets')->addChildren(
|
||||
Route::get('/([a-zA-Z0-9\-]+)\.(css|js)', 'serveComponent'),
|
||||
Route::get('/avatar/([0-9]+)(?:\.png)?', 'serveAvatar'),
|
||||
Route::get('/profile-background/([0-9]+)(?:\.png)?', 'serveProfileBackground'),
|
||||
),
|
||||
if(strpos($request->getUri()->getPath(), '.php') === false) {
|
||||
Router::addRoutes(
|
||||
// Home
|
||||
Route::get('/', 'index', 'Home'),
|
||||
|
||||
// Info
|
||||
Route::get('/info', 'index', 'Info'),
|
||||
Route::get('/info/([A-Za-z0-9_/]+)', 'page', 'Info'),
|
||||
// Assets
|
||||
Route::group('/assets', 'Assets')->addChildren(
|
||||
Route::get('/([a-zA-Z0-9\-]+)\.(css|js)', 'serveComponent'),
|
||||
Route::get('/avatar/([0-9]+)(?:\.png)?', 'serveAvatar'),
|
||||
Route::get('/profile-background/([0-9]+)(?:\.png)?', 'serveProfileBackground'),
|
||||
),
|
||||
|
||||
// Changelog
|
||||
Route::get('/changelog', 'index', 'Changelog')->addChildren(
|
||||
Route::get('.atom', 'feedAtom'),
|
||||
Route::get('.rss', 'feedRss'),
|
||||
Route::get('/change/([0-9]+)', 'change'),
|
||||
),
|
||||
// Info
|
||||
Route::get('/info', 'index', 'Info'),
|
||||
Route::get('/info/([A-Za-z0-9_\-/]+)', 'page', 'Info'),
|
||||
|
||||
// News
|
||||
Route::get('/news', 'index', 'News')->addChildren(
|
||||
Route::get('.atom', 'feedIndexAtom'),
|
||||
Route::get('.rss', 'feedIndexRss'),
|
||||
Route::get('/([0-9]+)', 'viewCategory'),
|
||||
Route::get('/([0-9]+).atom', 'feedCategoryAtom'),
|
||||
Route::get('/([0-9]+).rss', 'feedCategoryRss'),
|
||||
Route::get('/post/([0-9]+)', 'viewPost')
|
||||
),
|
||||
// Changelog
|
||||
Route::get('/changelog', 'index', 'Changelog')->addChildren(
|
||||
Route::get('.atom', 'feedAtom'),
|
||||
Route::get('.rss', 'feedRss'),
|
||||
Route::get('/change/([0-9]+)', 'change'),
|
||||
),
|
||||
|
||||
// Forum
|
||||
Route::group('/forum', 'Forum')->addChildren(
|
||||
Route::get('/mark-as-read', 'markAsReadGET')->addFilters('EnforceLogIn'),
|
||||
Route::post('/mark-as-read', 'markAsReadPOST')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
),
|
||||
// News
|
||||
Route::get('/news', 'index', 'News')->addChildren(
|
||||
Route::get('.atom', 'feedIndexAtom'),
|
||||
Route::get('.rss', 'feedIndexRss'),
|
||||
Route::get('/([0-9]+)', 'viewCategory'),
|
||||
Route::get('/([0-9]+).atom', 'feedCategoryAtom'),
|
||||
Route::get('/([0-9]+).rss', 'feedCategoryRss'),
|
||||
Route::get('/post/([0-9]+)', 'viewPost')
|
||||
),
|
||||
|
||||
// Sock Chat
|
||||
Route::create(['GET', 'POST'], '/_sockchat.php', 'phpFile', 'SockChat'),
|
||||
Route::group('/_sockchat', 'SockChat')->addChildren(
|
||||
Route::get('/emotes', 'emotes'),
|
||||
Route::get('/bans', 'bans'),
|
||||
Route::get('/login', 'login'),
|
||||
Route::post('/bump', 'bump'),
|
||||
Route::post('/verify', 'verify'),
|
||||
),
|
||||
// Forum
|
||||
Route::get('/forum', 'index', 'Forum.ForumIndex')->addChildren(
|
||||
Route::get('/mark-as-read', 'markAsRead')->addFilters('EnforceLogIn'),
|
||||
Route::post('/mark-as-read', 'markAsRead')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
|
||||
// Redirects
|
||||
Route::get('/index.php', url('index')),
|
||||
Route::get('/info.php', url('info')),
|
||||
Route::get('/settings.php', url('settings-index')),
|
||||
Route::get('/changelog.php', 'legacy', 'Changelog'),
|
||||
Route::get('/info.php/([A-Za-z0-9_/]+)', 'redir', 'Info'),
|
||||
Route::get('/auth.php', 'legacy', 'Auth'),
|
||||
Route::get('/news.php', 'legacy', 'News'),
|
||||
Route::get('/news.php/rss', 'legacy', 'News'),
|
||||
Route::get('/news.php/atom', 'legacy', 'News'),
|
||||
Route::get('/news/index.php', 'legacy', 'News'),
|
||||
Route::get('/news/category.php', 'legacy', 'News'),
|
||||
Route::get('/news/post.php', 'legacy', 'News'),
|
||||
Route::get('/news/feed.php', 'legacy', 'News'),
|
||||
Route::get('/news/feed.php/rss', 'legacy', 'News'),
|
||||
Route::get('/news/feed.php/atom', 'legacy', 'News'),
|
||||
Route::get('/user-assets.php', 'serveLegacy', 'Assets'),
|
||||
);
|
||||
Route::get('/([0-9]+)', 'category', 'Forum.ForumCategory')->addChildren(
|
||||
Route::get('/create-topic', 'createView')->addFilters('EnforceLogIn'),
|
||||
Route::post('/create-topic', 'createAction')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
),
|
||||
|
||||
Route::get('/topic/([0-9]+)', 'topic', 'Forum.ForumTopic')->addChildren(
|
||||
Route::get('/live', 'live')->addFilters('EnforceLogIn'),
|
||||
Route::post('/reply', 'reply')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
Route::post('/delete', 'delete')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
Route::post('/restore', 'restore')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
Route::post('/nuke', 'nuke')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
Route::post('/bump', 'bump')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
Route::post('/lock', 'lock')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
Route::post('/unlock', 'unlock')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
),
|
||||
|
||||
Route::get('/post/([0-9]+)', 'post', 'Forum.ForumPost')->addChildren(
|
||||
Route::post('/', 'edit')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
Route::post('/delete', 'delete')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
Route::post('/restore', 'restore')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
Route::post('/nuke', 'nuke')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
),
|
||||
|
||||
Route::post('/poll/([0-9]+)', 'vote', 'Forum.ForumPoll')->addFilters('EnforceLogIn', 'ValidateCsrf'),
|
||||
),
|
||||
|
||||
// Sock Chat
|
||||
Route::create(['GET', 'POST'], '/_sockchat.php', 'phpFile', 'SockChat'),
|
||||
Route::group('/_sockchat', 'SockChat')->addChildren(
|
||||
Route::get('/emotes', 'emotes'),
|
||||
Route::get('/bans', 'bans'),
|
||||
Route::get('/login', 'login'),
|
||||
Route::post('/bump', 'bump'),
|
||||
Route::post('/verify', 'verify'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Router::addRoutes(
|
||||
// Redirects
|
||||
Route::get('/index.php', url('index')),
|
||||
Route::get('/info.php', url('info')),
|
||||
Route::get('/settings.php', url('settings-index')),
|
||||
Route::get('/changelog.php', 'legacy', 'Changelog'),
|
||||
Route::get('/info.php/([A-Za-z0-9_\-/]+)', 'redir', 'Info'),
|
||||
Route::get('/auth.php', 'legacy', 'Auth'),
|
||||
Route::get('/news.php', 'legacy', 'News'),
|
||||
Route::get('/news.php/rss', 'legacy', 'News'),
|
||||
Route::get('/news.php/atom', 'legacy', 'News'),
|
||||
Route::get('/news/index.php', 'legacy', 'News'),
|
||||
Route::get('/news/category.php', 'legacy', 'News'),
|
||||
Route::get('/news/post.php', 'legacy', 'News'),
|
||||
Route::get('/news/feed.php', 'legacy', 'News'),
|
||||
Route::get('/news/feed.php/rss', 'legacy', 'News'),
|
||||
Route::get('/news/feed.php/atom', 'legacy', 'News'),
|
||||
Route::get('/user-assets.php', 'serveLegacy', 'Assets'),
|
||||
Route::create(['GET', 'POST'], '/forum/index.php', 'legacy', 'Forum.ForumIndex'),
|
||||
Route::get('/forum/forum.php', 'legacy', 'Forum.ForumCategory'),
|
||||
Route::get('/forum/topic.php', 'legacy', 'Forum.ForumTopic'),
|
||||
Route::get('/forum/post.php', 'legacy', 'Forum.ForumPost'),
|
||||
);
|
||||
}
|
||||
|
||||
$response = Router::handle($request);
|
||||
$response->setHeader('X-Powered-By', 'Misuzu');
|
||||
|
|
|
@ -344,50 +344,19 @@ switch($profileMode) {
|
|||
|
||||
case 'forum-topics':
|
||||
$template = 'profile.topics';
|
||||
$topicsCount = forum_topic_count_user($profileUser->getId(), $currentUserId);
|
||||
$topicsPagination = new Pagination($topicsCount, 20);
|
||||
|
||||
if(!$topicsPagination->hasValidOffset()) {
|
||||
echo render_error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$topics = forum_topic_listing_user(
|
||||
$profileUser->getId(), $currentUserId,
|
||||
$topicsPagination->getOffset(), $topicsPagination->getRange()
|
||||
);
|
||||
|
||||
Template::set([
|
||||
'title' => $profileUser->getUsername() . ' / topics',
|
||||
'canonical_url' => url('user-profile-forum-topics', ['user' => $profileUser->getId(), 'page' => Pagination::param()]),
|
||||
'profile_topics' => $topics,
|
||||
'profile_topics_pagination' => $topicsPagination,
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'forum-posts':
|
||||
$template = 'profile.posts';
|
||||
$postsCount = forum_post_count_user($profileUser->getId());
|
||||
$postsPagination = new Pagination($postsCount, 20);
|
||||
|
||||
if(!$postsPagination->hasValidOffset()) {
|
||||
echo render_error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$posts = forum_post_listing(
|
||||
$profileUser->getId(),
|
||||
$postsPagination->getOffset(),
|
||||
$postsPagination->getRange(),
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
Template::set([
|
||||
'title' => $profileUser->getUsername() . ' / posts',
|
||||
'canonical_url' => url('user-profile-forum-posts', ['user' => $profileUser->getId(), 'page' => Pagination::param()]),
|
||||
'profile_posts' => $posts,
|
||||
'profile_posts_pagination' => $postsPagination,
|
||||
]);
|
||||
break;
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Forum\ForumTopic;
|
||||
use Misuzu\Forum\ForumPost;
|
||||
use Misuzu\News\NewsPost;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
|
@ -9,8 +11,8 @@ require_once '../misuzu.php';
|
|||
$searchQuery = !empty($_GET['q']) && is_string($_GET['q']) ? $_GET['q'] : '';
|
||||
|
||||
if(!empty($searchQuery)) {
|
||||
$forumTopics = forum_topic_listing_search($searchQuery, User::hasCurrent() ? User::getCurrent()->getId() : 0);
|
||||
$forumPosts = forum_post_search($searchQuery);
|
||||
$forumTopics = ForumTopic::bySearchQuery($searchQuery);
|
||||
$forumPosts = ForumPost::bySearchQuery($searchQuery);
|
||||
$newsPosts = NewsPost::bySearchQuery($searchQuery);
|
||||
|
||||
$findUsers = DB::prepare(sprintf(
|
||||
|
|
|
@ -38,9 +38,10 @@ class Colour {
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getInherit(): bool {
|
||||
public function isInherit(): bool {
|
||||
return ($this->getRaw() & self::FLAG_INHERIT) > 0;
|
||||
}
|
||||
public function getInherit(): bool { return $this->isInherit(); }
|
||||
public function setInherit(bool $inherit): self {
|
||||
$raw = $this->getRaw();
|
||||
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -43,13 +43,12 @@ class DatabaseStatement {
|
|||
return $out ? $out : $default;
|
||||
}
|
||||
|
||||
private $hasExecuted = false;
|
||||
public function fetchObject(string $className = 'stdClass', ?array $args = null, $default = null) {
|
||||
$out = false;
|
||||
|
||||
if($this->isQuery || $this->execute()) {
|
||||
$out = $args === null ? $this->stmt->fetchObject($className) : $this->stmt->fetchObject($className, $args);
|
||||
}
|
||||
if(!$this->hasExecuted)
|
||||
$this->hasExecuted = $this->isQuery || $this->execute();
|
||||
|
||||
$out = $args === null ? $this->stmt->fetchObject($className) : $this->stmt->fetchObject($className, $args);
|
||||
return $out !== false ? $out : $default;
|
||||
}
|
||||
|
||||
|
|
587
src/Forum/ForumCategory.php
Normal file
587
src/Forum/ForumCategory.php
Normal file
|
@ -0,0 +1,587 @@
|
|||
<?php
|
||||
namespace Misuzu\Forum;
|
||||
|
||||
use Misuzu\Colour;
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Memoizer;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class ForumCategoryException extends ForumException {}
|
||||
class ForumCategoryNotFoundException extends ForumCategoryException {}
|
||||
|
||||
class ForumCategory {
|
||||
public const TYPE_DISCUSSION = 0;
|
||||
public const TYPE_CATEGORY = 1;
|
||||
public const TYPE_LINK = 2;
|
||||
public const TYPE_FEATURE = 3;
|
||||
|
||||
public const TYPES = [
|
||||
self::TYPE_DISCUSSION, self::TYPE_CATEGORY, self::TYPE_LINK, self::TYPE_FEATURE,
|
||||
];
|
||||
public const HAS_CHILDREN = [
|
||||
self::TYPE_DISCUSSION, self::TYPE_CATEGORY, self::TYPE_FEATURE,
|
||||
];
|
||||
public const HAS_TOPICS = [
|
||||
self::TYPE_DISCUSSION, self::TYPE_FEATURE,
|
||||
];
|
||||
public const HAS_PRIORITY_VOTES = [
|
||||
self::TYPE_FEATURE,
|
||||
];
|
||||
|
||||
public const ROOT_ID = 0;
|
||||
|
||||
// Database fields
|
||||
private $forum_id = -1;
|
||||
private $forum_order = 0;
|
||||
private $forum_parent = 0;
|
||||
private $forum_name = '';
|
||||
private $forum_type = self::TYPE_DISCUSSION;
|
||||
private $forum_description = null;
|
||||
private $forum_icon = null;
|
||||
private $forum_colour = null;
|
||||
private $forum_link = null;
|
||||
private $forum_link_clicks = null;
|
||||
private $forum_created = null;
|
||||
private $forum_archived = 0;
|
||||
private $forum_hidden = 0;
|
||||
private $forum_count_topics = 0;
|
||||
private $forum_count_posts = 0;
|
||||
|
||||
public const TABLE = 'forum_categories';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`forum_id`, %1$s.`forum_order`, %1$s.`forum_parent`, %1$s.`forum_name`, %1$s.`forum_type`, %1$s.`forum_description`'
|
||||
. ', %1$s.`forum_icon`, %1$s.`forum_colour`, %1$s.`forum_link`, %1$s.`forum_link_clicks`'
|
||||
. ', %1$s.`forum_archived`, %1$s.`forum_hidden`, %1$s.`forum_count_topics`, %1$s.`forum_count_posts`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`forum_created`) AS `forum_created`';
|
||||
|
||||
private $categoryColour = null;
|
||||
private $realColour = null;
|
||||
private $parentCategory = null;
|
||||
private $children = null;
|
||||
|
||||
public function getId(): int {
|
||||
return $this->forum_id < 0 ? -1 : $this->forum_id;
|
||||
}
|
||||
public function isRoot(): bool {
|
||||
return $this->forum_id === self::ROOT_ID;
|
||||
}
|
||||
|
||||
public function getOrder(): int {
|
||||
return $this->forum_order;
|
||||
}
|
||||
public function setOrder(int $order): self {
|
||||
$this->forum_order = $order;
|
||||
return $this;
|
||||
}
|
||||
public function moveBelow(self $other): self {
|
||||
$this->setOrder($other->getOrder() + 1);
|
||||
return $this;
|
||||
}
|
||||
public function moveAbove(self $other): self {
|
||||
$this->setOrder($other->getOrder() - 1);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentId(): int {
|
||||
return $this->forum_parent < 0 ? -1 : $this->forum_parent;
|
||||
}
|
||||
public function setParentId(int $otherId): self {
|
||||
$this->forum_parent = $otherId;
|
||||
$this->parentCategory = null;
|
||||
return $this;
|
||||
}
|
||||
public function hasParent(): bool {
|
||||
return $this->getParentId() > 0;
|
||||
}
|
||||
public function getParent(): self {
|
||||
if($this->parentCategory === null)
|
||||
$this->parentCategory = $this->hasParent() ? self::byId($this->getParentId()) : self::root();
|
||||
return $this->parentCategory;
|
||||
}
|
||||
public function setParent(?self $other): self {
|
||||
$this->forum_parent = $other === null ? 0 : $other->getId();
|
||||
$this->parentCategory = $other;
|
||||
return $this;
|
||||
}
|
||||
public function getParentTree(): array {
|
||||
$current = $this;
|
||||
$parents = [];
|
||||
while(!$current->isRoot())
|
||||
$parents[] = $current = $current->getParent();
|
||||
return array_reverse($parents);
|
||||
}
|
||||
|
||||
public function getUrl(): string {
|
||||
if($this->isRoot())
|
||||
return url('forum-index');
|
||||
return url('forum-category', ['forum' => $this->getId()]);
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->forum_name;
|
||||
}
|
||||
public function setName(string $name): self {
|
||||
$this->forum_name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): int {
|
||||
return $this->forum_type;
|
||||
}
|
||||
public function setType(int $type): self {
|
||||
$this->forum_type = $type;
|
||||
return $this;
|
||||
}
|
||||
public function isDiscussionForum(): bool { return $this->getType() === self::TYPE_DISCUSSION; }
|
||||
public function isCategoryForum(): bool { return $this->getType() === self::TYPE_CATEGORY; }
|
||||
public function isFeatureForum(): bool { return $this->getType() === self::TYPE_FEATURE; }
|
||||
public function isLink(): bool { return $this->getType() === self::TYPE_LINK; }
|
||||
public function canHaveChildren(): bool {
|
||||
return in_array($this->getType(), self::HAS_CHILDREN);
|
||||
}
|
||||
public function canHaveTopics(): bool {
|
||||
return in_array($this->getType(), self::HAS_TOPICS);
|
||||
}
|
||||
public function canHavePriorityVotes(): bool {
|
||||
return in_array($this->getType(), self::HAS_PRIORITY_VOTES);
|
||||
}
|
||||
|
||||
public function getDescription(): string {
|
||||
return $this->forum_description ?? '';
|
||||
}
|
||||
public function hasDescription(): bool {
|
||||
return !empty($this->forum_description);
|
||||
}
|
||||
public function setDescription(string $description): self {
|
||||
$this->forum_description = empty($description) ? null : $description;
|
||||
return $this;
|
||||
}
|
||||
public function getParsedDescription(): string {
|
||||
return nl2br($this->getDescription());
|
||||
}
|
||||
|
||||
public function getIcon(): string {
|
||||
$icon = $this->getRealIcon();
|
||||
if(!empty($icon))
|
||||
return $icon;
|
||||
|
||||
if($this->isArchived())
|
||||
return 'fas fa-archive fa-fw';
|
||||
|
||||
switch($this->getType()) {
|
||||
case self::TYPE_FEATURE:
|
||||
return 'fas fa-star fa-fw';
|
||||
case self::TYPE_LINK:
|
||||
return 'fas fa-link fa-fw';
|
||||
case self::TYPE_CATEGORY:
|
||||
return 'fas fa-folder fa-fw';
|
||||
}
|
||||
|
||||
return 'fas fa-comments fa-fw';
|
||||
}
|
||||
public function getRealIcon(): string {
|
||||
return $this->forum_icon ?? '';
|
||||
}
|
||||
public function hasIcon(): bool {
|
||||
return !empty($this->forum_icon);
|
||||
}
|
||||
public function setIcon(string $icon): self {
|
||||
$this->forum_icon = empty($icon) ? null : $icon;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getColourRaw(): int {
|
||||
return $this->forum_colour ?? 0x40000000;
|
||||
}
|
||||
public function setColourRaw(?int $raw): self {
|
||||
$this->forum_colour = $raw;
|
||||
$this->realColour = null;
|
||||
$this->categoryColour = null;
|
||||
return $this;
|
||||
}
|
||||
public function getColour(): Colour { // Swaps parent colour in if no category colour is present
|
||||
if($this->realColour === null) {
|
||||
$this->realColour = $this->getCategoryColour();
|
||||
if($this->realColour->getInherit() && $this->hasParent())
|
||||
$this->realColour = $this->getParent()->getColour();
|
||||
}
|
||||
return $this->realColour;
|
||||
}
|
||||
public function getCategoryColour(): Colour {
|
||||
if($this->categoryColour === null)
|
||||
$this->categoryColour = new Colour($this->getColourRaw());
|
||||
return $this->categoryColour;
|
||||
}
|
||||
public function setColour(Colour $colour): self {
|
||||
return $this->setColourRaw($colour === null ? null : $colour->getRaw());
|
||||
}
|
||||
|
||||
public function getLink(): string {
|
||||
return $this->forum_link ?? '';
|
||||
}
|
||||
public function hasLink(): bool {
|
||||
return !empty($this->forum_link);
|
||||
}
|
||||
public function setLink(string $link): self {
|
||||
$this->forum_link = empty($link) ? null : $link;
|
||||
}
|
||||
|
||||
public function getLinkClicks(): int {
|
||||
return $this->forum_link_clicks ?? -1;
|
||||
}
|
||||
public function shouldCountLinkClicks(): bool {
|
||||
return $this->isLink() && $this->getLinkClicks() >= 0;
|
||||
}
|
||||
public function setCountLinkClicks(bool $state): self {
|
||||
if($this->isLink() && $this->shouldCountLinkClicks() !== $state) {
|
||||
$this->forum_link_clicks = $state ? 0 : null;
|
||||
|
||||
// forum_link_clicks is not affected by the save method so we must save
|
||||
DB::prepare(
|
||||
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
|
||||
. ' SET `forum_link_clicks` = :clicks'
|
||||
. ' WHERE `forum_id` = :category'
|
||||
) ->bind('category', $this->getId())
|
||||
->bind('clicks', $this->forum_link_clicks)
|
||||
->execute();
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
public function increaseLinkClicks(): void {
|
||||
if($this->shouldCountLinkClicks()) {
|
||||
$this->forum_link_clicks = $this->getLinkClicks() + 1;
|
||||
DB::prepare(
|
||||
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
|
||||
. ' SET `forum_link_clicks` = `forum_link_clicks` + 1'
|
||||
. ' WHERE `forum_id` = :category AND `forum_type` = ' . self::TYPE_LINK
|
||||
)->bind('category', $this->getId())->execute();
|
||||
}
|
||||
}
|
||||
|
||||
public function getCreatedTime(): int {
|
||||
return $this->forum_created === null ? -1 : $this->forum_created;
|
||||
}
|
||||
|
||||
public function isArchived(): bool {
|
||||
return boolval($this->forum_archived);
|
||||
}
|
||||
public function setArchived(bool $archived): self {
|
||||
$this->forum_archived = $archived ? 1 : 0;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isHidden(): bool {
|
||||
return boolval($this->forum_hidden);
|
||||
}
|
||||
public function setHidden(bool $hidden): self {
|
||||
$this->forum_hidden = $hidden ? 1 : 0;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTopicCount(): int {
|
||||
return $this->forum_count_topics ?? 0;
|
||||
}
|
||||
public function getPostCount(): int {
|
||||
return $this->forum_count_posts ?? 0;
|
||||
}
|
||||
public function increaseTopicPostCount(bool $hasTopic): void {
|
||||
if($this->isLink() || $this->isRoot())
|
||||
return;
|
||||
if($this->hasParent())
|
||||
$this->getParent()->increaseTopicPostCount($hasTopic);
|
||||
|
||||
if($hasTopic)
|
||||
$this->forum_count_topics = $this->getTopicCount() + 1;
|
||||
$this->forum_count_posts = $this->getPostCount() + 1;
|
||||
|
||||
DB::prepare(
|
||||
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
|
||||
. ' SET `forum_count_posts` = `forum_count_posts` + 1'
|
||||
. ($hasTopic ? ', `forum_count_topics` = `forum_count_topics` + 1' : '')
|
||||
. ' WHERE `forum_id` = :category'
|
||||
)->bind('category', $this->getId())->execute();
|
||||
}
|
||||
|
||||
// Param is fucking hackjob
|
||||
// -1 = no check
|
||||
// null = guest
|
||||
// User = user
|
||||
public function getChildren(/* ?User */ $viewer = -1): array {
|
||||
if(!$this->canHaveChildren())
|
||||
return [];
|
||||
if($this->children === null)
|
||||
$this->children = self::all($this);
|
||||
if($viewer === null || $viewer instanceof User) {
|
||||
$children = [];
|
||||
foreach($this->children as $child)
|
||||
if($child->canView($viewer))
|
||||
$children[] = $child;
|
||||
return $children;
|
||||
}
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
public function getActualTopicCount(bool $includeDeleted = false): int {
|
||||
if(!$this->canHaveTopics())
|
||||
return 0;
|
||||
return ForumTopic::countByCategory($this, $includeDeleted);
|
||||
}
|
||||
public function getActualPostCount(bool $includeDeleted = false): int {
|
||||
if(!$this->canHaveTopics())
|
||||
return 0;
|
||||
return ForumPost::countByCategory($this, $includeDeleted);
|
||||
}
|
||||
|
||||
public function getTopics(bool $includeDeleted = false, ?Pagination $pagination = null): array {
|
||||
if(!$this->canHaveTopics())
|
||||
return [];
|
||||
return ForumTopic::byCategory($this, $includeDeleted, $pagination);
|
||||
}
|
||||
|
||||
public function checkLegacyPermission(?User $user, int $perm, bool $strict = false): bool {
|
||||
return forum_perms_check_user(
|
||||
MSZ_FORUM_PERMS_GENERAL,
|
||||
$this->isRoot() ? null : $this->getId(),
|
||||
$user === null ? 0 : $user->getId(),
|
||||
$perm, $strict
|
||||
);
|
||||
}
|
||||
public function canView(?User $user): bool {
|
||||
return $this->checkLegacyPermission($user, MSZ_FORUM_PERM_SET_READ);
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
$lastTopic = ForumTopic::byCategoryLast($this);
|
||||
$children = $this->getChildren($viewer);
|
||||
|
||||
foreach($children as $child) {
|
||||
$topic = $child->getLatestTopic($viewer);
|
||||
if($topic !== null && ($lastTopic === null || $topic->getBumpedTime() > $lastTopic->getBumpedTime()))
|
||||
$lastTopic = $topic;
|
||||
}
|
||||
|
||||
return $lastTopic;
|
||||
}
|
||||
|
||||
// This function is really fucking expensive and should only be called by cron
|
||||
// Optimise this as much as possible at some point
|
||||
public function synchronise(bool $save = true): array {
|
||||
$topics = 0; $posts = 0; $topicStats = [];
|
||||
|
||||
$children = $this->getChildren();
|
||||
foreach($children as $child) {
|
||||
$stats = $child->synchronise($save);
|
||||
$topics += $stats['topics'];
|
||||
$posts += $stats['posts'];
|
||||
if(empty($topicStats) || (!empty($stats['topic_stats']) && $stats['topic_stats']['last_post_time'] > $topicStats['last_post_time']))
|
||||
$topicStats = $stats['topic_stats'];
|
||||
}
|
||||
|
||||
$getCounts = DB::prepare(
|
||||
'SELECT :forum as `target_forum_id`, ('
|
||||
. ' SELECT COUNT(`topic_id`)'
|
||||
. ' FROM `msz_forum_topics`'
|
||||
. ' WHERE `forum_id` = `target_forum_id`'
|
||||
. ' AND `topic_deleted` IS NULL'
|
||||
. ') AS `topics`, ('
|
||||
. ' SELECT COUNT(`post_id`)'
|
||||
. ' FROM `msz_forum_posts`'
|
||||
. ' WHERE `forum_id` = `target_forum_id`'
|
||||
. ' AND `post_deleted` IS NULL'
|
||||
. ') AS `posts`'
|
||||
);
|
||||
$getCounts->bind('forum', $this->getId());
|
||||
$counts = $getCounts->fetch();
|
||||
$topics += $counts['topics'];
|
||||
$posts += $counts['posts'];
|
||||
|
||||
foreach($this->getTopics() as $topic) {
|
||||
$stats = $topic->synchronise($save);
|
||||
if(empty($topicStats) || $stats['last_post_time'] > $topicStats['last_post_time'])
|
||||
$topicStats = $stats;
|
||||
}
|
||||
|
||||
if($save && !$this->isRoot()) {
|
||||
$setCounts = DB::prepare(
|
||||
'UPDATE `msz_forum_categories`'
|
||||
. ' SET `forum_count_topics` = :topics, `forum_count_posts` = :posts'
|
||||
. ' WHERE `forum_id` = :forum'
|
||||
);
|
||||
$setCounts->bind('forum', $this->getId());
|
||||
$setCounts->bind('topics', $topics);
|
||||
$setCounts->bind('posts', $posts);
|
||||
$setCounts->execute();
|
||||
}
|
||||
|
||||
return [
|
||||
'topics' => $topics,
|
||||
'posts' => $posts,
|
||||
'topic_stats' => $topicStats,
|
||||
];
|
||||
}
|
||||
|
||||
public static function root(): self {
|
||||
static $root = null;
|
||||
if($root === null) {
|
||||
$root = new static;
|
||||
$root->forum_id = self::ROOT_ID;
|
||||
$root->forum_name = 'Forums';
|
||||
$root->forum_type = self::TYPE_CATEGORY;
|
||||
$root->forum_created = 1359324884;
|
||||
}
|
||||
return $root;
|
||||
}
|
||||
|
||||
public function save(): void {
|
||||
if($this->isRoot())
|
||||
return;
|
||||
$isInsert = $this->getId() < 0;
|
||||
|
||||
if($isInsert) {
|
||||
$save = DB::prepare(
|
||||
'INSERT INTO `' . DB::PREFIX . self::TABLE . '` ('
|
||||
. '`forum_order`, `forum_parent`, `forum_name`, `forum_type`, `forum_description`, `forum_icon`'
|
||||
. ', `forum_colour`, `forum_link`, `forum_archived`, `forum_hidden`'
|
||||
. ') VALUES (:order, :parent, :name, :type, :desc, :icon, :colour, :link, :archived, :hidden)'
|
||||
);
|
||||
} else {
|
||||
$save = DB::prepare(
|
||||
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
|
||||
. ' SET `forum_order` = :order, `forum_parent` = :parent, `forum_name` = :name, `forum_type` = :type'
|
||||
. ', `forum_description` = :desc, `forum_icon` = :icon, `forum_colour` = :colour, `forum_link` = :link'
|
||||
. ', `forum_archived` = :archived, `forum_hidden` = :hidden'
|
||||
. ' WHERE `forum_id` = :category'
|
||||
)->bind('category', $this->getId());
|
||||
}
|
||||
|
||||
$save->bind('order', $this->forum_order)
|
||||
->bind('parent', $this->forum_parent)
|
||||
->bind('name', $this->forum_name)
|
||||
->bind('type', $this->forum_type)
|
||||
->bind('desc', $this->forum_description)
|
||||
->bind('icon', $this->forum_icon)
|
||||
->bind('colour', $this->forum_colour)
|
||||
->bind('link', $this->forum_link)
|
||||
->bind('archived', $this->forum_archived)
|
||||
->bind('hidden', $this->forum_hidden);
|
||||
|
||||
if($isInsert) {
|
||||
$this->forum_id = $save->executeGetId();
|
||||
$this->forum_created = time();
|
||||
} else $save->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 byId(int $categoryId): self {
|
||||
if($categoryId === self::ROOT_ID)
|
||||
return self::root();
|
||||
|
||||
return self::memoizer()->find($categoryId, function() use ($categoryId) {
|
||||
$object = DB::prepare(self::byQueryBase() . ' WHERE `forum_id` = :category')
|
||||
->bind('category', $categoryId)
|
||||
->fetchObject(self::class);
|
||||
if(!$object)
|
||||
throw new ForumCategoryNotFoundException;
|
||||
return $object;
|
||||
});
|
||||
}
|
||||
public static function all(?self $parent = null): array {
|
||||
$getObjects = DB::prepare(
|
||||
self::byQueryBase()
|
||||
. ($parent === null ? '' : ' WHERE `forum_parent` = :parent')
|
||||
. ' ORDER BY `forum_order`'
|
||||
);
|
||||
|
||||
if($parent !== null)
|
||||
$getObjects->bind('parent', $parent->getId());
|
||||
|
||||
$objects = [];
|
||||
$memoizer = self::memoizer();
|
||||
while($object = $getObjects->fetchObject(self::class))
|
||||
$memoizer->insert($objects[] = $object);
|
||||
return $objects;
|
||||
}
|
||||
}
|
6
src/Forum/ForumException.php
Normal file
6
src/Forum/ForumException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace Misuzu\Forum;
|
||||
|
||||
use Misuzu\MisuzuException;
|
||||
|
||||
class ForumException extends MisuzuException {}
|
103
src/Forum/ForumLeaderboard.php
Normal file
103
src/Forum/ForumLeaderboard.php
Normal file
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
namespace Misuzu\Forum;
|
||||
|
||||
use Misuzu\DB;
|
||||
|
||||
final class ForumLeaderboard {
|
||||
public const START_YEAR = 2018;
|
||||
public const START_MONTH = 12;
|
||||
public const CATEGORY_ALL = 0;
|
||||
|
||||
public static function isValidYear(?int $year): bool {
|
||||
return !is_null($year) && $year >= self::START_YEAR && $year <= date('Y');
|
||||
}
|
||||
|
||||
public static function isValidMonth(?int $year, ?int $month): bool {
|
||||
if(is_null($month) || !self::isValidYear($year) || $month < 1 || $month > 12)
|
||||
return false;
|
||||
|
||||
$combo = sprintf('%04d%02d', $year, $month);
|
||||
$start = sprintf('%04d%02d', self::START_YEAR, self::START_MONTH);
|
||||
$current = date('Ym');
|
||||
|
||||
return $combo >= $start && $combo <= $current;
|
||||
}
|
||||
|
||||
public static function categories(): array {
|
||||
$categories = [
|
||||
self::CATEGORY_ALL => 'All Time',
|
||||
];
|
||||
|
||||
$currentYear = date('Y');
|
||||
$currentMonth = date('m');
|
||||
|
||||
for($i = $currentYear; $i >= self::START_YEAR; $i--) {
|
||||
$categories[$i] = sprintf('Leaderboard %d', $i);
|
||||
}
|
||||
|
||||
for($i = $currentYear, $j = $currentMonth;;) {
|
||||
$categories[sprintf('%d%02d', $i, $j)] = sprintf('Leaderboard %d-%02d', $i, $j);
|
||||
|
||||
if($j <= 1) {
|
||||
$i--; $j = 12;
|
||||
} else $j--;
|
||||
|
||||
if($i <= self::START_YEAR && $j < self::START_MONTH)
|
||||
break;
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
public static function listing(
|
||||
?int $year = null,
|
||||
?int $month = null,
|
||||
array $unrankedForums = [],
|
||||
array $unrankedTopics = []
|
||||
): array {
|
||||
$hasYear = self::isValidYear($year);
|
||||
$hasMonth = $hasYear && self::isValidMonth($year, $month);
|
||||
$unrankedForums = implode(',', $unrankedForums);
|
||||
$unrankedTopics = implode(',', $unrankedTopics);
|
||||
|
||||
$rawLeaderboard = DB::query(sprintf(
|
||||
'
|
||||
SELECT
|
||||
u.`user_id`, u.`username`,
|
||||
COUNT(fp.`post_id`) as `posts`
|
||||
FROM `msz_users` AS u
|
||||
INNER JOIN `msz_forum_posts` AS fp
|
||||
ON fp.`user_id` = u.`user_id`
|
||||
WHERE fp.`post_deleted` IS NULL
|
||||
%s %s %s
|
||||
GROUP BY u.`user_id`
|
||||
HAVING `posts` > 0
|
||||
ORDER BY `posts` DESC
|
||||
',
|
||||
$unrankedForums ? sprintf('AND fp.`forum_id` NOT IN (%s)', $unrankedForums) : '',
|
||||
$unrankedTopics ? sprintf('AND fp.`topic_id` NOT IN (%s)', $unrankedTopics) : '',
|
||||
!$hasYear ? '' : sprintf(
|
||||
'AND DATE(fp.`post_created`) BETWEEN \'%1$04d-%2$02d-01\' AND \'%1$04d-%3$02d-31\'',
|
||||
$year,
|
||||
$hasMonth ? $month : 1,
|
||||
$hasMonth ? $month : 12
|
||||
)
|
||||
))->fetchAll();
|
||||
|
||||
$leaderboard = [];
|
||||
$ranking = 0;
|
||||
$lastPosts = null;
|
||||
|
||||
foreach($rawLeaderboard as $entry) {
|
||||
if(is_null($lastPosts) || $lastPosts > $entry['posts']) {
|
||||
$ranking++;
|
||||
$lastPosts = $entry['posts'];
|
||||
}
|
||||
|
||||
$entry['rank'] = $ranking;
|
||||
$leaderboard[] = $entry;
|
||||
}
|
||||
|
||||
return $leaderboard;
|
||||
}
|
||||
}
|
166
src/Forum/ForumPoll.php
Normal file
166
src/Forum/ForumPoll.php
Normal file
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
namespace Misuzu\Forum;
|
||||
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Memoizer;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class ForumPollException extends ForumException {}
|
||||
class ForumPollNotFoundException extends ForumPollException {}
|
||||
|
||||
class ForumPoll {
|
||||
// Database fields
|
||||
private $poll_id = -1;
|
||||
private $topic_id = null;
|
||||
private $poll_max_votes = 0;
|
||||
private $poll_expires = null;
|
||||
private $poll_preview_results = 0;
|
||||
private $poll_change_vote = 0;
|
||||
|
||||
public const TABLE = 'forum_polls';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`poll_id`, %1$s.`topic_id`, %1$s.`poll_max_votes`, %1$s.`poll_preview_results`, %1$s.`poll_change_vote`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`poll_expires`) AS `poll_expires`';
|
||||
|
||||
private $topic = null;
|
||||
private $options = null;
|
||||
private $answers = [];
|
||||
private $voteCount = -1;
|
||||
|
||||
public function getId(): int {
|
||||
return $this->poll_id < 1 ? -1 : $this->poll_id;
|
||||
}
|
||||
|
||||
public function getTopicId(): int {
|
||||
return $this->topic_id < 1 ? -1 : $this->topic_id;
|
||||
}
|
||||
public function setTopicId(?int $topicId): self {
|
||||
$this->topic_id = $topicId < 1 ? null : $topicId;
|
||||
$this->topic = null;
|
||||
return $this;
|
||||
}
|
||||
public function hasTopic(): bool {
|
||||
return $this->getTopicId() > 0;
|
||||
}
|
||||
public function getTopic(): ForumTopic {
|
||||
if($this->topic === null)
|
||||
$this->topic = ForumTopic::byId($this->getTopicId());
|
||||
return $this->topic;
|
||||
}
|
||||
public function setTopic(?ForumTopic $topic): self {
|
||||
$this->topic_id = $topic === null ? null : $topic->getId();
|
||||
$this->topic = $topic;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMaxVotes(): int {
|
||||
return max(0, $this->poll_max_votes);
|
||||
}
|
||||
public function setMaxVotes(int $maxVotes): self {
|
||||
$this->poll_max_votes = max(0, $maxVotes);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getExpiresTime(): int {
|
||||
return $this->poll_expires === null ? -1 : $this->poll_expires;
|
||||
}
|
||||
public function hasExpired(): bool {
|
||||
return $this->getExpiresTime() >= time();
|
||||
}
|
||||
public function canExpire(): bool {
|
||||
return $this->getExpiresTime() >= 0;
|
||||
}
|
||||
public function setExpiresTime(int $expires): self {
|
||||
$this->poll_expires = $expires < 0 ? null : $expires;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function canPreviewResults(): bool {
|
||||
return boolval($this->poll_preview_results);
|
||||
}
|
||||
public function setPreviewResults(bool $canPreview): self {
|
||||
$this->poll_preview_results = $canPreview ? 1 : 0;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function canChangeVote(): bool {
|
||||
return boolval($this->poll_change_vote);
|
||||
}
|
||||
public function setChangeVote(bool $canChange): self {
|
||||
$this->poll_change_vote = $canChange ? 1 : 0;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVotes(): int {
|
||||
if($this->voteCount < 0)
|
||||
$this->voteCount = ForumPollAnswer::countByPoll($this);
|
||||
return $this->voteCount;
|
||||
}
|
||||
|
||||
public function getOptions(): array {
|
||||
if($this->options === null)
|
||||
$this->options = ForumPollOption::byPoll($this);
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function getAnswers(?User $user): array {
|
||||
if($user === null)
|
||||
return [];
|
||||
$userId = $user->getId();
|
||||
if(!isset($this->answers[$userId]))
|
||||
$this->answers[$userId] = ForumPollAnswer::byExact($user, $this);
|
||||
return $this->answers[$userId];
|
||||
}
|
||||
public function hasVoted(?User $user): bool {
|
||||
if($user === null)
|
||||
return false;
|
||||
$userId = $user->getId();
|
||||
if(!isset($this->answers[$userId]))
|
||||
return !empty($this->getAnswers($user));
|
||||
return !empty($this->answers[$userId]);
|
||||
}
|
||||
|
||||
public function canVoteOnPoll(?User $user): bool {
|
||||
if($user === null)
|
||||
return false;
|
||||
if(!$this->hasTopic()) // introduce generic poll vote permission?
|
||||
return true;
|
||||
return forum_perms_check_user(
|
||||
MSZ_FORUM_PERMS_GENERAL,
|
||||
$this->getTopic()->getCategory()->getId(),
|
||||
$user->getId(),
|
||||
MSZ_FORUM_PERM_SET_READ
|
||||
);
|
||||
}
|
||||
|
||||
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 byId(int $pollId): self {
|
||||
return self::memoizer()->find($pollId, function() use ($pollId) {
|
||||
$object = DB::prepare(self::byQueryBase() . ' WHERE `poll_id` = :poll')
|
||||
->bind('poll', $pollId)
|
||||
->fetchObject(self::class);
|
||||
if(!$object)
|
||||
throw new ForumPollNotFoundException;
|
||||
return $object;
|
||||
});
|
||||
}
|
||||
public static function byTopic(ForumTopic $topic): array {
|
||||
$getObjects = DB::prepare(self::byQueryBase() . ' WHERE `topic_id` = :topic')
|
||||
->bind('topic', $topic->getId());
|
||||
|
||||
$objects = [];
|
||||
$memoizer = self::memoizer();
|
||||
while($object = $getObjects->fetchObject(self::class))
|
||||
$memoizer->insert($objects[] = $object);
|
||||
return $objects;
|
||||
}
|
||||
}
|
75
src/Forum/ForumPollAnswer.php
Normal file
75
src/Forum/ForumPollAnswer.php
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
namespace Misuzu\Forum;
|
||||
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class ForumPollAnswer {
|
||||
// Database fields
|
||||
private $user_id = -1;
|
||||
private $poll_id = -1;
|
||||
private $option_id = -1;
|
||||
|
||||
public const TABLE = 'forum_polls_answers';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`user_id`, %1$s.`poll_id`, %1$s.`option_id`';
|
||||
|
||||
private $user = null;
|
||||
private $poll = null;
|
||||
private $option = null;
|
||||
|
||||
public function getUserId(): int {
|
||||
return $this->user_id < 1 ? -1 : $this->user_id;
|
||||
}
|
||||
public function getUser(): User {
|
||||
if($this->user === null)
|
||||
$this->user = User::byId($this->getUserId());
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getPollId(): int {
|
||||
return $this->poll_id < 1 ? -1 : $this->poll_id;
|
||||
}
|
||||
public function getPoll(): ForumPoll {
|
||||
if($this->poll === null)
|
||||
$this->poll = ForumPoll::byId($this->getPollId());
|
||||
return $this->poll;
|
||||
}
|
||||
|
||||
public function getOptionId(): int {
|
||||
return $this->option_id < 1 ? -1 : $this->option_id;
|
||||
}
|
||||
public function getOption(): ForumPollOption {
|
||||
if($this->option === null)
|
||||
$this->option = ForumPollOption::byId($this->getOptionId());
|
||||
return $this->option;
|
||||
}
|
||||
|
||||
public function hasAnswer(): bool {
|
||||
return $this->getOptionId() > 0;
|
||||
}
|
||||
|
||||
private static function countQueryBase(): string {
|
||||
return sprintf(self::QUERY_SELECT, sprintf('COUNT(*)', self::TABLE));
|
||||
}
|
||||
public static function countByPoll(ForumPoll $poll): int {
|
||||
return (int)DB::prepare(
|
||||
self::countQueryBase() . ' WHERE `poll_id` = :poll'
|
||||
)->bind('poll', $poll->getId())->fetchColumn();
|
||||
}
|
||||
public static function countByOption(ForumPollOption $option): int {
|
||||
return (int)DB::prepare(
|
||||
self::countQueryBase() . ' WHERE `option_id` = :option'
|
||||
)->bind('option', $option->getId())->fetchColumn();
|
||||
}
|
||||
|
||||
private static function byQueryBase(): string {
|
||||
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
|
||||
}
|
||||
public static function byExact(User $user, ForumPoll $poll): array {
|
||||
return DB::prepare(self::byQueryBase() . ' WHERE `poll_id` = :poll AND `user_id` = :user')
|
||||
->bind('user', $user->getId())
|
||||
->bind('poll', $poll->getId())
|
||||
->fetchObjects(self::class);
|
||||
}
|
||||
}
|
63
src/Forum/ForumPollOption.php
Normal file
63
src/Forum/ForumPollOption.php
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
namespace Misuzu\Forum;
|
||||
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class ForumPollOption {
|
||||
// Database fields
|
||||
private $option_id = -1;
|
||||
private $poll_id = -1;
|
||||
private $option_text = null;
|
||||
|
||||
public const TABLE = 'forum_polls_options';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`option_id`, %1$s.`poll_id`, %1$s.`option_text`';
|
||||
|
||||
private $poll = null;
|
||||
private $voteCount = -1;
|
||||
|
||||
public function getId(): int {
|
||||
return $this->option_id < 1 ? -1 : $this->option_id;
|
||||
}
|
||||
|
||||
public function getPollId(): int {
|
||||
return $this->poll_id < 1 ? -1 : $this->poll_id;
|
||||
}
|
||||
public function getPoll(): ForumPoll {
|
||||
if($this->poll === null)
|
||||
$this->poll = ForumPoll::byId($this->getPollId());
|
||||
return $this->poll;
|
||||
}
|
||||
|
||||
public function getText(): string {
|
||||
return $this->option_text ?? '';
|
||||
}
|
||||
|
||||
public function getPercentage(): float {
|
||||
return $this->getVotes() / $this->getPoll()->getVotes();
|
||||
}
|
||||
|
||||
public function getVotes(): int {
|
||||
if($this->voteCount < 0)
|
||||
$this->voteCount = ForumPollAnswer::countByOption($this);
|
||||
return $this->voteCount;
|
||||
}
|
||||
|
||||
public function hasVotedFor(?User $user): bool {
|
||||
if($user === null)
|
||||
return false;
|
||||
return array_find($this->getPoll()->getAnswers($user), function($answer) {
|
||||
return $answer->getOptionId() === $this->getId();
|
||||
}) !== null;
|
||||
}
|
||||
|
||||
private static function byQueryBase(): string {
|
||||
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
|
||||
}
|
||||
public static function byPoll(ForumPoll $poll): array {
|
||||
return DB::prepare(self::byQueryBase() . ' WHERE `poll_id` = :poll')
|
||||
->bind('poll', $poll->getId())
|
||||
->fetchObjects(self::class);
|
||||
}
|
||||
}
|
488
src/Forum/ForumPost.php
Normal file
488
src/Forum/ForumPost.php
Normal file
|
@ -0,0 +1,488 @@
|
|||
<?php
|
||||
namespace Misuzu\Forum;
|
||||
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Memoizer;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Parsers\Parser;
|
||||
use Misuzu\Users\User;
|
||||
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;
|
||||
|
||||
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;
|
||||
private $forum_id = -1;
|
||||
private $user_id = null;
|
||||
private $post_ip = '::1';
|
||||
private $post_text = '';
|
||||
private $post_parse = Parser::BBCODE;
|
||||
private $post_display_signature = 1;
|
||||
private $post_created = null;
|
||||
private $post_edited = null;
|
||||
private $post_deleted = null;
|
||||
|
||||
public const TABLE = 'forum_posts';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`post_id`, %1$s.`topic_id`, %1$s.`forum_id`, %1$s.`user_id`, %1$s.`post_text`, %1$s.`post_parse`, %1$s.`post_display_signature`'
|
||||
. ', INET6_NTOA(%1$s.`post_ip`) AS `post_ip`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`post_created`) AS `post_created`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`post_edited`) AS `post_edited`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`post_deleted`) AS `post_deleted`';
|
||||
|
||||
private $topic = null;
|
||||
private $category = null;
|
||||
private $user = null;
|
||||
private $userLookedUp = false;
|
||||
|
||||
public function getId(): int {
|
||||
return $this->post_id < 1 ? -1 : $this->post_id;
|
||||
}
|
||||
|
||||
public function getTopicId(): int {
|
||||
return $this->topic_id < 1 ? -1 : $this->topic_id;
|
||||
}
|
||||
public function setTopicId(int $topicId): self {
|
||||
$this->topic_id = $topicId;
|
||||
$this->topic = null;
|
||||
return $this;
|
||||
}
|
||||
public function getTopic(): ForumTopic {
|
||||
if($this->topic === null)
|
||||
$this->topic = ForumTopic::byId($this->getTopicId());
|
||||
return $this->topic;
|
||||
}
|
||||
public function setTopic(ForumTopic $topic): self {
|
||||
$this->topic_id = $topic->getId();
|
||||
$this->topic = $topic;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCategoryId(): int {
|
||||
return $this->forum_id < 1 ? -1 : $this->forum_id;
|
||||
}
|
||||
public function setCategoryId(int $categoryId): self {
|
||||
$this->forum_id = $categoryId;
|
||||
$this->category = null;
|
||||
return $this;
|
||||
}
|
||||
public function getCategory(): ForumCategory {
|
||||
if($this->category === null)
|
||||
$this->category = ForumCategory::byId($this->getCategoryId());
|
||||
return $this->category;
|
||||
}
|
||||
public function setCategory(ForumCategory $category): self {
|
||||
$this->forum_id = $category->getId();
|
||||
$this->category = $category;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUserId(): int {
|
||||
return $this->user_id < 1 ? -1 : $this->user_id;
|
||||
}
|
||||
public function setUserId(?int $userId): self {
|
||||
$this->user_id = $userId < 1 ? null : $userId;
|
||||
$this->user = null;
|
||||
$this->userLookedUp = false;
|
||||
return $this;
|
||||
}
|
||||
public function hasUser(): bool {
|
||||
return $this->getUserId() > 0;
|
||||
}
|
||||
public function getUser(): ?User {
|
||||
if(!$this->userLookedUp) {
|
||||
$this->userLookedUp = true;
|
||||
try {
|
||||
$this->user = User::byId($this->getUserId());
|
||||
} catch(UserNotFoundException $ex) {}
|
||||
}
|
||||
return $this->user;
|
||||
}
|
||||
public function setUser(?User $user): self {
|
||||
$this->user_id = $user === null ? null : $user->getId();
|
||||
$this->user = $user;
|
||||
$this->userLookedUp = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
public function getParsedBody(): string {
|
||||
return Parser::instance($this->getBodyParser())->parseText(htmlspecialchars($this->getBody()));
|
||||
}
|
||||
public function getFirstBodyParagraph(): string {
|
||||
return htmlspecialchars(first_paragraph($this->getBody()));
|
||||
}
|
||||
public function setBody(string $body): self {
|
||||
$this->post_text = empty($body) ? null : $body;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBodyParser(): int {
|
||||
return $this->post_parse;
|
||||
}
|
||||
public function setBodyParser(int $parser): self {
|
||||
$this->post_parse = $parser;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBodyClasses(): string {
|
||||
if($this->getBodyParser() === Parser::MARKDOWN)
|
||||
return 'markdown';
|
||||
return '';
|
||||
}
|
||||
|
||||
public function shouldDisplaySignature(): bool {
|
||||
return boolval($this->post_display_signature);
|
||||
}
|
||||
public function setDisplaySignature(bool $display): self {
|
||||
$this->post_display_signature = $display ? 1 : 0;
|
||||
return $this;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
public function isEdited(): bool {
|
||||
return $this->getEditedTime() >= 0;
|
||||
}
|
||||
public function bumpEdited(): self {
|
||||
$this->post_edited = time();
|
||||
return $this;
|
||||
}
|
||||
public function stripEdited(): self {
|
||||
$this->post_edited = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDeletedTime(): int {
|
||||
return $this->post_deleted === null ? -1 : $this->post_deleted;
|
||||
}
|
||||
public function isDeleted(): bool {
|
||||
return $this->getDeletedTime() >= 0;
|
||||
}
|
||||
|
||||
public function isOpeningPost(): bool {
|
||||
return $this->getTopic()->isOpeningPost($this);
|
||||
}
|
||||
|
||||
public function isTopicAuthor(): bool {
|
||||
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;
|
||||
// check if user can view deleted posts
|
||||
return true;
|
||||
}
|
||||
|
||||
// complete this implementation
|
||||
public function canBeEdited(?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)
|
||||
return 'short';
|
||||
if($length > self::BODY_MAX_LENGTH)
|
||||
return 'long';
|
||||
return '';
|
||||
}
|
||||
public static function bodyValidationErrorString(string $error): string {
|
||||
switch($error) {
|
||||
case 'short':
|
||||
return sprintf('Post body was too short, it has to be at least %d characters!', self::BODY_MIN_LENGTH);
|
||||
case 'long':
|
||||
return sprintf("Post body was too long, it can't be longer than %d characters!", self::BODY_MAX_LENGTH);
|
||||
case '':
|
||||
return 'Post body is correctly formatted!';
|
||||
default:
|
||||
return 'Post body is incorrectly formatted.';
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
return;
|
||||
DB::prepare(
|
||||
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
|
||||
. ' SET `post_deleted` = NOW()'
|
||||
. ' WHERE `topic_id` = :topic'
|
||||
. ' AND `post_deleted` IS NULL'
|
||||
)->bind('topic', $topic->getId())->execute();
|
||||
}
|
||||
public static function restoreTopic(ForumTopic $topic): void {
|
||||
// This looks like an error but it's not, run this before restoring the topic
|
||||
if(!$topic->isDeleted())
|
||||
return;
|
||||
DB::prepare(
|
||||
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
|
||||
. ' SET `post_deleted` = NULL'
|
||||
. ' WHERE `topic_id` = :topic'
|
||||
. ' AND `post_deleted` = FROM_UNIXTIME(:deleted)'
|
||||
)->bind('topic', $topic->getId())->bind('deleted', $topic->getDeletedTime())->execute();
|
||||
}
|
||||
public static function nukeTopic(ForumTopic $topic): void { // Does this need to exist? Happens implicitly through foreign keys.
|
||||
// Hard deleting should only be allowed if the topic is already soft deleted
|
||||
if(!$topic->isDeleted())
|
||||
return;
|
||||
DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `topic_id` = :topic')
|
||||
->bind('topic', $topic->getId())
|
||||
->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));
|
||||
}
|
||||
public static function countByCategory(ForumCategory $category, bool $includeDeleted = false): int {
|
||||
return (int)DB::prepare(
|
||||
self::countQueryBase()
|
||||
. ' WHERE `forum_id` = :category'
|
||||
. ($includeDeleted ? '' : ' AND `post_deleted` IS NULL')
|
||||
)->bind('category', $category->getId())->fetchColumn();
|
||||
}
|
||||
public static function countByTopic(ForumTopic $topic, bool $includeDeleted = false): int {
|
||||
return (int)DB::prepare(
|
||||
self::countQueryBase()
|
||||
. ' WHERE `topic_id` = :topic'
|
||||
. ($includeDeleted ? '' : ' AND `post_deleted` IS NULL')
|
||||
)->bind('topic', $topic->getId())->fetchColumn();
|
||||
}
|
||||
|
||||
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 byId(int $postId): self {
|
||||
return self::memoizer()->find($postId, function() use ($postId) {
|
||||
$object = DB::prepare(self::byQueryBase() . ' WHERE `post_id` = :post')
|
||||
->bind('post', $postId)
|
||||
->fetchObject(self::class);
|
||||
if(!$object)
|
||||
throw new ForumPostNotFoundException;
|
||||
return $object;
|
||||
});
|
||||
}
|
||||
public static function byTopic(ForumTopic $topic, bool $includeDeleted = false, ?Pagination $pagination = null): array {
|
||||
$query = self::byQueryBase()
|
||||
. ' WHERE `topic_id` = :topic'
|
||||
. ($includeDeleted ? '' : ' AND `post_deleted` IS NULL')
|
||||
. ' ORDER BY `post_id`';
|
||||
|
||||
if($pagination !== null)
|
||||
$query .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getObjects = DB::prepare($query)
|
||||
->bind('topic', $topic->getId());
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
588
src/Forum/ForumTopic.php
Normal file
588
src/Forum/ForumTopic.php
Normal file
|
@ -0,0 +1,588 @@
|
|||
<?php
|
||||
namespace Misuzu\Forum;
|
||||
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Memoizer;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class ForumTopicException extends ForumException {}
|
||||
class ForumTopicNotFoundException extends ForumTopicException {}
|
||||
class ForumTopicCreationFailedException extends ForumTopicException {}
|
||||
class ForumTopicUpdateFailedException extends ForumTopicException {}
|
||||
|
||||
class ForumTopic {
|
||||
public const TYPE_DISCUSSION = 0;
|
||||
public const TYPE_STICKY = 1;
|
||||
public const TYPE_ANNOUNCEMENT = 2;
|
||||
public const TYPE_GLOBAL_ANNOUNCEMENT = 3;
|
||||
|
||||
public const TYPES = [
|
||||
self::TYPE_DISCUSSION,
|
||||
self::TYPE_STICKY,
|
||||
self::TYPE_ANNOUNCEMENT,
|
||||
self::TYPE_GLOBAL_ANNOUNCEMENT,
|
||||
];
|
||||
|
||||
public const TYPE_ORDER = [
|
||||
self::TYPE_GLOBAL_ANNOUNCEMENT,
|
||||
self::TYPE_ANNOUNCEMENT,
|
||||
self::TYPE_STICKY,
|
||||
self::TYPE_DISCUSSION,
|
||||
];
|
||||
|
||||
public const TYPE_IMPORTANT = [
|
||||
self::TYPE_STICKY,
|
||||
self::TYPE_ANNOUNCEMENT,
|
||||
self::TYPE_GLOBAL_ANNOUNCEMENT,
|
||||
];
|
||||
|
||||
public const TITLE_MIN_LENGTH = 3;
|
||||
public const TITLE_MAX_LENGTH = 100;
|
||||
|
||||
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;
|
||||
private $user_id = null;
|
||||
private $topic_type = self::TYPE_DISCUSSION;
|
||||
private $topic_title = '';
|
||||
private $topic_priority = 0;
|
||||
private $topic_count_posts = 0;
|
||||
private $topic_count_views = 0;
|
||||
private $topic_post_first = null;
|
||||
private $topic_post_last = null;
|
||||
private $topic_created = null;
|
||||
private $topic_bumped = null;
|
||||
private $topic_deleted = null;
|
||||
private $topic_locked = null;
|
||||
|
||||
public const TABLE = 'forum_topics';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`topic_id`, %1$s.`forum_id`, %1$s.`user_id`, %1$s.`topic_type`, %1$s.`topic_title`'
|
||||
. ', %1$s.`topic_count_posts`, %1$s.`topic_count_views`, %1$s.`topic_post_first`, %1$s.`topic_post_last`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`topic_created`) AS `topic_created`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`topic_bumped`) AS `topic_bumped`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`topic_deleted`) AS `topic_deleted`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`topic_locked`) AS `topic_locked`';
|
||||
|
||||
private $category = null;
|
||||
private $user = null;
|
||||
private $firstPost = -1;
|
||||
private $lastPost = -1;
|
||||
private $priorityVotes = null;
|
||||
private $polls = [];
|
||||
|
||||
public function getId(): int {
|
||||
return $this->topic_id < 1 ? -1 : $this->topic_id;
|
||||
}
|
||||
|
||||
public function getCategoryId(): int {
|
||||
return $this->forum_id < 1 ? -1 : $this->forum_id;
|
||||
}
|
||||
public function setCategoryId(int $categoryId): self {
|
||||
$this->forum_id = $categoryId;
|
||||
$this->category = null;
|
||||
return $this;
|
||||
}
|
||||
public function getCategory(): ForumCategory {
|
||||
if($this->category === null)
|
||||
$this->category = ForumCategory::byId($this->getCategoryId());
|
||||
return $this->category;
|
||||
}
|
||||
public function setCategory(ForumCategory $category): self {
|
||||
$this->forum_id = $category->getId();
|
||||
$this->category = $category;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUserId(): int {
|
||||
return $this->user_id < 1 ? -1 : $this->user_id;
|
||||
}
|
||||
public function setUserId(?int $userId): self {
|
||||
$this->user_id = $userId < 1 ? null : $userId;
|
||||
$this->user = null;
|
||||
return $this;
|
||||
}
|
||||
public function getUser(): ?User {
|
||||
if($this->user === null && ($userId = $this->getUserId()) > 0)
|
||||
$this->user = User::byId($userId);
|
||||
return $this->user;
|
||||
}
|
||||
public function hasUser(): bool {
|
||||
return $this->getUserId() > 0;
|
||||
}
|
||||
public function setUser(?User $user): self {
|
||||
$this->user_id = $user === null ? null : $user->getId();
|
||||
$this->user = $user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): int {
|
||||
return $this->topic_type;
|
||||
}
|
||||
public function setType(int $type): self {
|
||||
$this->topic_type = $type;
|
||||
return $this;
|
||||
}
|
||||
public function isNormal(): bool { return $this->getType() === self::TYPE_DISCUSSION; }
|
||||
public function isSticky(): bool { return $this->getType() === self::TYPE_STICKY; }
|
||||
public function isAnnouncement(): bool { return $this->getType() === self::TYPE_ANNOUNCEMENT; }
|
||||
public function isGlobalAnnouncement(): bool { return $this->getType() === self::TYPE_GLOBAL_ANNOUNCEMENT; }
|
||||
|
||||
public function isImportant(): bool {
|
||||
return in_array($this->getType(), self::TYPE_IMPORTANT);
|
||||
}
|
||||
|
||||
public function hasPriorityVoting(): bool {
|
||||
return $this->getCategory()->canHavePriorityVotes();
|
||||
}
|
||||
|
||||
public function getIcon(?User $viewer = null): string {
|
||||
if($this->isDeleted())
|
||||
return 'fas fa-trash-alt fa-fw';
|
||||
|
||||
if($this->isGlobalAnnouncement() || $this->isAnnouncement())
|
||||
return 'fas fa-bullhorn fa-fw';
|
||||
if($this->isSticky())
|
||||
return 'fas fa-thumbtack fa-fw';
|
||||
|
||||
if($this->isLocked())
|
||||
return 'fas fa-lock fa-fw';
|
||||
|
||||
if($this->hasPriorityVoting())
|
||||
return 'far fa-star fa-fw';
|
||||
|
||||
return ($viewer === null || $this->hasRead($viewer) ? 'far' : 'fas') . ' fa-comment fa-fw';
|
||||
}
|
||||
|
||||
public function getTitle(): string {
|
||||
return $this->topic_title ?? '';
|
||||
}
|
||||
public function setTitle(string $title): self {
|
||||
$this->topic_title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPriority(): int {
|
||||
return $this->topic_priority < 1 ? 0 : $this->topic_priority;
|
||||
}
|
||||
|
||||
public function getPostCount(): int {
|
||||
return $this->topic_count_posts;
|
||||
}
|
||||
public function getPageCount(int $postsPerPage = 10): int {
|
||||
return ceil($this->getPostCount() / $postsPerPage);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
public function hasFirstPost(): bool {
|
||||
return $this->getFirstPostId() > 0;
|
||||
}
|
||||
public function getFirstPost(): ?ForumPost {
|
||||
if($this->firstPost === -1) {
|
||||
if(!$this->hasFirstPost())
|
||||
return null;
|
||||
try {
|
||||
$this->firstPost = ForumPost::byId($this->getFirstPostId());
|
||||
} catch(ForumPostNotFoundException $ex) {
|
||||
$this->firstPost = null;
|
||||
}
|
||||
}
|
||||
return $this->firstPost;
|
||||
}
|
||||
|
||||
public function getLastPostId(): int {
|
||||
return $this->topic_post_last < 1 ? -1 : $this->topic_post_last;
|
||||
}
|
||||
public function hasLastPost(): bool {
|
||||
return $this->getLastPostId() > 0;
|
||||
}
|
||||
public function getLastPost(): ?ForumPost {
|
||||
if($this->lastPost === -1) {
|
||||
if(!$this->hasLastPost())
|
||||
return null;
|
||||
try {
|
||||
$this->lastPost = ForumPost::byId($this->getLastPostId());
|
||||
} catch(ForumPostNotFoundException $ex) {
|
||||
$this->lastPost = null;
|
||||
}
|
||||
}
|
||||
return $this->lastPost;
|
||||
}
|
||||
|
||||
public function getCreatedTime(): int {
|
||||
return $this->topic_created === null ? -1 : $this->topic_created;
|
||||
}
|
||||
|
||||
public function getBumpedTime(): int {
|
||||
return $this->topic_bumped === null ? -1 : $this->topic_bumped;
|
||||
}
|
||||
public function bumpTopic(): void {
|
||||
if($this->isDeleted())
|
||||
return;
|
||||
$this->topic_bumped = time();
|
||||
DB::prepare(
|
||||
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
|
||||
. ' SET `topic_bumped` = NOW()'
|
||||
. ' WHERE `topic_id` = :topic'
|
||||
. ' AND `topic_deleted` IS NULL'
|
||||
)->bind('topic', $this->getId())->execute();
|
||||
}
|
||||
|
||||
public function getDeletedTime(): int {
|
||||
return $this->topic_deleted === null ? -1 : $this->topic_deleted;
|
||||
}
|
||||
public function isDeleted(): bool {
|
||||
return $this->getDeletedTime() >= 0;
|
||||
}
|
||||
|
||||
public function getLockedTime(): int {
|
||||
return $this->topic_locked === null ? -1 : $this->topic_locked;
|
||||
}
|
||||
public function isLocked(): bool {
|
||||
return $this->getLockedTime() >= 0;
|
||||
}
|
||||
public function setLocked(bool $locked): self {
|
||||
if($this->isLocked() !== $locked)
|
||||
$this->topic_locked = $locked ? time() : null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isArchived(): bool {
|
||||
return $this->getCategory()->isArchived();
|
||||
}
|
||||
|
||||
public function getActualPostCount(bool $includeDeleted = false): int {
|
||||
return ForumPost::countByTopic($this, $includeDeleted);
|
||||
}
|
||||
public function getPosts(bool $includeDeleted = false, ?Pagination $pagination = null): array {
|
||||
return ForumPost::byTopic($this, $includeDeleted, $pagination);
|
||||
}
|
||||
|
||||
public function getPolls(): array {
|
||||
if($this->polls === null)
|
||||
$this->polls = ForumPoll::byTopic($this);
|
||||
return $this->polls;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
public function markRead(User $user): void {
|
||||
if(!$this->hasRead($user))
|
||||
$this->incrementViewCount();
|
||||
ForumTopicTrack::bump($this, $user);
|
||||
}
|
||||
|
||||
public function hasParticipated(?User $user): bool {
|
||||
return $user !== null;
|
||||
}
|
||||
|
||||
public function isOpeningPost(ForumPost $post): bool {
|
||||
$firstPost = $this->getFirstPost();
|
||||
return $firstPost !== null && $firstPost->getId() === $post->getId();
|
||||
}
|
||||
public function isTopicAuthor(?User $user): bool {
|
||||
if($user === null)
|
||||
return false;
|
||||
return $user->getId() === $this->getUser()->getId();
|
||||
}
|
||||
|
||||
public function getPriorityVotes(): array {
|
||||
if($this->priorityVotes === null)
|
||||
$this->priorityVotes = ForumTopicPriority::byTopic($this);
|
||||
return $this->priorityVotes;
|
||||
}
|
||||
|
||||
public function canVoteOnPriority(?User $user): bool {
|
||||
if($user === null || !$this->hasPriorityVoting())
|
||||
return false;
|
||||
// shouldn't there be an actual permission for this?
|
||||
return $this->getCategory()->canView($user);
|
||||
}
|
||||
|
||||
public function canBeDeleted(User $user): string {
|
||||
if(false) // check if viewable
|
||||
return 'view';
|
||||
|
||||
// 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';
|
||||
if($this->getActualPostCount(true) >= self::DELETE_POST_LIMIT)
|
||||
return 'posts';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
public static function canBeDeletedErrorString(string $error): string {
|
||||
switch($error) {
|
||||
case 'view':
|
||||
return 'This topic doesn\'t exist.';
|
||||
case 'deleted':
|
||||
return 'This topic has already been marked as deleted.';
|
||||
case 'permission':
|
||||
return 'You aren\'t allowed to this topic.';
|
||||
case 'owner':
|
||||
return 'You can only delete your own topics.';
|
||||
case 'age':
|
||||
return 'This topic is too old to be deleted. Ask a moderator to remove it if you deem it absolutely necessary.';
|
||||
case 'posts':
|
||||
return 'This topic has too many replies to be deleted. Ask a moderator to remove it if you deem it absolutely necessary.';
|
||||
case '':
|
||||
return 'Topic can be deleted!';
|
||||
default:
|
||||
return 'Topic cannot be deleted.';
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(): void {
|
||||
if($this->isDeleted())
|
||||
return;
|
||||
$this->topic_deleted = time();
|
||||
DB::prepare('UPDATE `' . DB::PREFIX . self::TABLE . '` SET `topic_deleted` = NOW() WHERE `topic_id` = :topic')
|
||||
->bind('topic', $this->getId())
|
||||
->execute();
|
||||
ForumPost::deleteTopic($this);
|
||||
}
|
||||
public function restore(): void {
|
||||
if(!$this->isDeleted())
|
||||
return;
|
||||
ForumPost::restoreTopic($this);
|
||||
DB::prepare('UPDATE `' . DB::PREFIX . self::TABLE . '` SET `topic_deleted` = NULL WHERE `topic_id` = :topic')
|
||||
->bind('topic', $this->getId())
|
||||
->execute();
|
||||
$this->topic_deleted = null;
|
||||
}
|
||||
public function nuke(): void {
|
||||
if(!$this->isDeleted())
|
||||
return;
|
||||
DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `topic_id` = :topic')
|
||||
->bind('topic', $this->getId())
|
||||
->execute();
|
||||
//ForumPost::nukeTopic($this);
|
||||
}
|
||||
|
||||
public static function create(ForumCategory $category, User $user, string $title, int $type = self::TYPE_DISCUSSION): ForumTopic {
|
||||
$create = DB::prepare(
|
||||
'INSERT INTO `msz_forum_topics` (`forum_id`, `user_id`, `topic_title`, `topic_type`) VALUES (:forum, :user, :title, :type)'
|
||||
)->bind('forum', $category->getId())->bind('user', $user->getId())
|
||||
->bind('title', $title)->bind('type', $type)
|
||||
->execute();
|
||||
if(!$create)
|
||||
throw new ForumTopicCreationFailedException;
|
||||
$topicId = DB::lastId();
|
||||
if($topicId < 1)
|
||||
throw new ForumTopicCreationFailedException;
|
||||
|
||||
try {
|
||||
return self::byId($topicId);
|
||||
} catch(ForumTopicNotFoundException $ex) {
|
||||
throw new ForumTopicCreationFailedException;
|
||||
}
|
||||
}
|
||||
|
||||
public function update(): void {
|
||||
if($this->getId() < 1)
|
||||
throw new ForumTopicUpdateFailedException;
|
||||
|
||||
if(!DB::prepare(
|
||||
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
|
||||
. ' SET `topic_title` = :title,'
|
||||
. ' `topic_type` = :type'
|
||||
. ' WHERE `topic_id` = :topic'
|
||||
)->bind('topic', $this->getId())
|
||||
->bind('title', $this->getTitle())
|
||||
->bind('type', $this->getType())
|
||||
->execute())
|
||||
throw new ForumTopicUpdateFailedException;
|
||||
}
|
||||
|
||||
public function synchronise(bool $save = true): array {
|
||||
$stats = DB::prepare(
|
||||
'SELECT :topic AS `topic`, ('
|
||||
. 'SELECT MIN(`post_id`) FROM `msz_forum_posts` WHERE `topic_id` = `topic`' // this shouldn't be deleteable without nuking the topic
|
||||
. ') AS `first_post`, ('
|
||||
. 'SELECT MAX(`post_id`) FROM `msz_forum_posts` WHERE `topic_id` = `topic` AND `post_deleted` IS NULL'
|
||||
. ') AS `last_post`, ('
|
||||
. 'SELECT COUNT(*) FROM `msz_forum_posts` WHERE `topic_id` = `topic` AND `post_deleted` IS NULL'
|
||||
. ') AS `posts`, ('
|
||||
. 'SELECT UNIX_TIMESTAMP(`post_created`) FROM `msz_forum_posts` WHERE `post_id` = `last_post`'
|
||||
. ') AS `last_post_time`'
|
||||
)->bind('topic', $this->getId())->fetch();
|
||||
|
||||
if($save) {
|
||||
$this->topic_post_first = $stats['first_post'];
|
||||
$this->topic_post_last = $stats['last_post'];
|
||||
$this->topic_count_posts = $stats['posts'];
|
||||
DB::prepare(
|
||||
'UPDATE `msz_forum_topics`'
|
||||
. ' SET `topic_post_first` = :first'
|
||||
. ', `topic_post_last` = :last'
|
||||
. ', `topic_count_posts` = :posts'
|
||||
. ' WHERE `topic_id` = :topic'
|
||||
) ->bind('first', $this->topic_post_first)
|
||||
->bind('last', $this->topic_post_last)
|
||||
->bind('posts', $this->topic_count_posts)
|
||||
->bind('topic', $this->getId())
|
||||
->execute();
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
public static function validateTitle(string $title): string {
|
||||
$length = mb_strlen(trim($title));
|
||||
if($length < self::TITLE_MIN_LENGTH)
|
||||
return 'short';
|
||||
if($length > self::TITLE_MAX_LENGTH)
|
||||
return 'long';
|
||||
return '';
|
||||
}
|
||||
public static function titleValidationErrorString(string $error): string {
|
||||
switch($error) {
|
||||
case 'short':
|
||||
return sprintf('Topic title was too short, it has to be at least %d characters!', self::TITLE_MIN_LENGTH);
|
||||
case 'long':
|
||||
return sprintf("Topic title was too long, it can't be longer than %d characters!", self::TITLE_MAX_LENGTH);
|
||||
case '':
|
||||
return 'Topic title is correctly formatted!';
|
||||
default:
|
||||
return 'Topic title is incorrectly formatted.';
|
||||
}
|
||||
}
|
||||
|
||||
private static function countQueryBase(): string {
|
||||
return sprintf(self::QUERY_SELECT, sprintf('COUNT(*)', self::TABLE));
|
||||
}
|
||||
public static function countByCategory(ForumCategory $category, bool $includeDeleted = false): int {
|
||||
return (int)DB::prepare(
|
||||
self::countQueryBase()
|
||||
. ' WHERE `forum_id` = :category'
|
||||
. ($includeDeleted ? '' : ' AND `topic_deleted` IS NULL')
|
||||
)->bind('category', $category->getId())->fetchColumn();
|
||||
}
|
||||
|
||||
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 byId(int $topicId): self {
|
||||
return self::memoizer()->find($topicId, function() use ($topicId) {
|
||||
$object = DB::prepare(self::byQueryBase() . ' WHERE `topic_id` = :topic')
|
||||
->bind('topic', $topicId)
|
||||
->fetchObject(self::class);
|
||||
if(!$object)
|
||||
throw new ForumTopicNotFoundException;
|
||||
return $object;
|
||||
});
|
||||
}
|
||||
public static function byCategoryLast(ForumCategory $category): ?self {
|
||||
return self::memoizer()->find(function($topic) use ($category) {
|
||||
// This doesn't actually do what is advertised, but should be fine for the time being.
|
||||
return $topic->getCategory()->getId() === $category->getId() && !$topic->isDeleted();
|
||||
}, function() use ($category) {
|
||||
return DB::prepare(
|
||||
self::byQueryBase()
|
||||
. ' WHERE `forum_id` = :category AND `topic_deleted` IS NULL'
|
||||
. ' ORDER BY `topic_bumped` DESC'
|
||||
. ' LIMIT 1'
|
||||
)->bind('category', $category->getId())->fetchObject(self::class);
|
||||
});
|
||||
}
|
||||
public static function byCategory(ForumCategory $category, bool $includeDeleted = false, ?Pagination $pagination = null): array {
|
||||
if(!$category->canHaveTopics())
|
||||
return [];
|
||||
|
||||
$query = self::byQueryBase()
|
||||
. ' WHERE `forum_id` = :category'
|
||||
. ($includeDeleted ? '' : ' AND `topic_deleted` IS NULL')
|
||||
. ' ORDER BY FIELD(`topic_type`, ' . implode(',', self::TYPE_ORDER) . ')';
|
||||
|
||||
//if($category->canHavePriorityVotes())
|
||||
// $query .= ', `topic_priority` DESC';
|
||||
|
||||
$query .= ', `topic_bumped` DESC';
|
||||
|
||||
if($pagination !== null)
|
||||
$query .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getObjects = DB::prepare($query)
|
||||
->bind('category', $category->getId());
|
||||
|
||||
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;
|
||||
}
|
||||
public static function bySearchQuery(string $search, bool $includeDeleted = false, ?Pagination $pagination = null): array {
|
||||
$query = self::byQueryBase()
|
||||
. ' WHERE MATCH(`topic_title`) AGAINST (:search IN NATURAL LANGUAGE MODE)'
|
||||
. ($includeDeleted ? '' : ' AND `topic_deleted` IS NULL')
|
||||
. ' ORDER BY FIELD(`topic_type`, ' . implode(',', self::TYPE_ORDER) . '), `topic_bumped` DESC';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
50
src/Forum/ForumTopicPriority.php
Normal file
50
src/Forum/ForumTopicPriority.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
namespace Misuzu\Forum;
|
||||
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class ForumTopicPriority {
|
||||
// Database fields
|
||||
private $topic_id = -1;
|
||||
private $user_id = -1;
|
||||
private $topic_priority = 0;
|
||||
|
||||
public const TABLE = 'forum_topics_priority';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`topic_id`, %1$s.`user_id`, %1$s.`topic_priority`';
|
||||
|
||||
private $topic = null;
|
||||
private $user = null;
|
||||
|
||||
public function getTopicId(): int {
|
||||
return $this->topic_id < 1 ? -1 : $this->topic_id;
|
||||
}
|
||||
public function getTopic(): ForumTopic {
|
||||
if($this->topic === null)
|
||||
$this->topic = ForumTopic::byId($this->getTopicId());
|
||||
return $this->topic;
|
||||
}
|
||||
|
||||
public function getUserId(): int {
|
||||
return $this->user_id < 1 ? -1 : $this->user_id;
|
||||
}
|
||||
public function getUser(): User {
|
||||
if($this->user === null)
|
||||
$this->user = User::byId($this->getUserId());
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getPriority(): int {
|
||||
return $this->topic_priority < 1 ? -1 : $this->topic_priority;
|
||||
}
|
||||
|
||||
private static function byQueryBase(): string {
|
||||
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
|
||||
}
|
||||
public static function byTopic(ForumTopic $topic): array {
|
||||
return DB::prepare(self::byQueryBase() . ' WHERE `topic_id` = :topic')
|
||||
->bind('topic', $topic->getId())
|
||||
->fetchObjects(self::class);
|
||||
}
|
||||
}
|
105
src/Forum/ForumTopicTrack.php
Normal file
105
src/Forum/ForumTopicTrack.php
Normal file
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
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;
|
||||
private $topic_id = -1;
|
||||
private $forum_id = -1;
|
||||
private $track_last_read = null;
|
||||
|
||||
public const TABLE = 'forum_topics_track';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`user_id`, %1$s.`topic_id`, %1$s.`forum_id`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`track_last_read`) AS `track_last_read`';
|
||||
|
||||
private $user = null;
|
||||
private $topic = null;
|
||||
private $category = null;
|
||||
|
||||
public function getUserId(): int {
|
||||
return $this->user_id < 1 ? -1 : $this->user_id;
|
||||
}
|
||||
public function getUser(): User {
|
||||
if($this->user === null)
|
||||
$this->user = User::byId($this->getUserId());
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getTopicId(): int {
|
||||
return $this->topic_id < 1 ? -1 : $this->topic_id;
|
||||
}
|
||||
public function getTopic(): ForumTopic {
|
||||
if($this->topic === null)
|
||||
$this->topic = ForumTopic::byId($this->getTopicId());
|
||||
return $this->topic;
|
||||
}
|
||||
|
||||
public function getCategoryId(): int {
|
||||
return $this->forum_id < 1 ? -1 : $this->forum_id;
|
||||
}
|
||||
public function getCategory(): ForumCategory {
|
||||
if($this->category === null)
|
||||
$this->category = ForumCategory::byId($this->getCategoryId());
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
0
src/Forum/ForumTrack.php
Normal file
0
src/Forum/ForumTrack.php
Normal file
|
@ -1,537 +0,0 @@
|
|||
<?php
|
||||
define('MSZ_FORUM_TYPE_DISCUSSION', 0);
|
||||
define('MSZ_FORUM_TYPE_CATEGORY', 1);
|
||||
define('MSZ_FORUM_TYPE_LINK', 2);
|
||||
define('MSZ_FORUM_TYPE_FEATURE', 3);
|
||||
define('MSZ_FORUM_TYPES', [
|
||||
MSZ_FORUM_TYPE_DISCUSSION,
|
||||
MSZ_FORUM_TYPE_CATEGORY,
|
||||
MSZ_FORUM_TYPE_LINK,
|
||||
MSZ_FORUM_TYPE_FEATURE,
|
||||
]);
|
||||
|
||||
define('MSZ_FORUM_MAY_HAVE_CHILDREN', [
|
||||
MSZ_FORUM_TYPE_DISCUSSION,
|
||||
MSZ_FORUM_TYPE_CATEGORY,
|
||||
MSZ_FORUM_TYPE_FEATURE,
|
||||
]);
|
||||
|
||||
define('MSZ_FORUM_MAY_HAVE_TOPICS', [
|
||||
MSZ_FORUM_TYPE_DISCUSSION,
|
||||
MSZ_FORUM_TYPE_FEATURE,
|
||||
]);
|
||||
|
||||
define('MSZ_FORUM_HAS_PRIORITY_VOTING', [
|
||||
MSZ_FORUM_TYPE_FEATURE,
|
||||
]);
|
||||
|
||||
define('MSZ_FORUM_ROOT', 0);
|
||||
define('MSZ_FORUM_ROOT_DATA', [ // should be compatible with the data fetched in forum_get_root_categories
|
||||
'forum_id' => MSZ_FORUM_ROOT,
|
||||
'forum_name' => 'Forums',
|
||||
'forum_children' => 0,
|
||||
'forum_type' => MSZ_FORUM_TYPE_CATEGORY,
|
||||
'forum_colour' => null,
|
||||
'forum_permissions' => MSZ_FORUM_PERM_SET_READ,
|
||||
]);
|
||||
|
||||
function forum_is_valid_type(int $type): bool {
|
||||
return in_array($type, MSZ_FORUM_TYPES, true);
|
||||
}
|
||||
|
||||
function forum_may_have_children(int $forumType): bool {
|
||||
return in_array($forumType, MSZ_FORUM_MAY_HAVE_CHILDREN);
|
||||
}
|
||||
|
||||
function forum_may_have_topics(int $forumType): bool {
|
||||
return in_array($forumType, MSZ_FORUM_MAY_HAVE_TOPICS);
|
||||
}
|
||||
|
||||
function forum_has_priority_voting(int $forumType): bool {
|
||||
return in_array($forumType, MSZ_FORUM_HAS_PRIORITY_VOTING);
|
||||
}
|
||||
|
||||
function forum_get(int $forumId, bool $showDeleted = false): array {
|
||||
$getForum = \Misuzu\DB::prepare(sprintf(
|
||||
'
|
||||
SELECT
|
||||
`forum_id`, `forum_name`, `forum_type`, `forum_link`, `forum_archived`,
|
||||
`forum_link_clicks`, `forum_parent`, `forum_colour`, `forum_icon`,
|
||||
(
|
||||
SELECT COUNT(`topic_id`)
|
||||
FROM `msz_forum_topics`
|
||||
WHERE `forum_id` = f.`forum_id`
|
||||
%1$s
|
||||
) as `forum_topic_count`
|
||||
FROM `msz_forum_categories` as f
|
||||
WHERE `forum_id` = :forum_id
|
||||
',
|
||||
$showDeleted ? '' : 'AND `topic_deleted` IS NULL'
|
||||
));
|
||||
$getForum->bind('forum_id', $forumId);
|
||||
return $getForum->fetch();
|
||||
}
|
||||
|
||||
function forum_get_root_categories(int $userId): array {
|
||||
$getCategories = \Misuzu\DB::prepare(sprintf(
|
||||
'
|
||||
SELECT
|
||||
f.`forum_id`, f.`forum_name`, f.`forum_type`, f.`forum_colour`, f.`forum_icon`,
|
||||
(
|
||||
SELECT COUNT(`forum_id`)
|
||||
FROM `msz_forum_categories` AS sf
|
||||
WHERE sf.`forum_parent` = f.`forum_id`
|
||||
) AS `forum_children`
|
||||
FROM `msz_forum_categories` AS f
|
||||
WHERE f.`forum_parent` = 0
|
||||
AND f.`forum_type` = %1$d
|
||||
AND f.`forum_hidden` = 0
|
||||
GROUP BY f.`forum_id`
|
||||
ORDER BY f.`forum_order`
|
||||
',
|
||||
MSZ_FORUM_TYPE_CATEGORY
|
||||
));
|
||||
$categories = array_merge([MSZ_FORUM_ROOT_DATA], $getCategories->fetchAll());
|
||||
|
||||
$getRootForumCount = \Misuzu\DB::prepare(sprintf(
|
||||
"
|
||||
SELECT COUNT(`forum_id`)
|
||||
FROM `msz_forum_categories`
|
||||
WHERE `forum_parent` = %d
|
||||
AND `forum_type` != %d
|
||||
",
|
||||
MSZ_FORUM_ROOT,
|
||||
MSZ_FORUM_TYPE_CATEGORY
|
||||
));
|
||||
$categories[0]['forum_children'] = (int)$getRootForumCount->fetchColumn();
|
||||
|
||||
foreach($categories as $key => $category) {
|
||||
$categories[$key]['forum_permissions'] = $perms = forum_perms_get_user($category['forum_id'], $userId)[MSZ_FORUM_PERMS_GENERAL];
|
||||
|
||||
if(!perms_check($perms, MSZ_FORUM_PERM_SET_READ)) {
|
||||
unset($categories[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$categories[$key] = array_merge(
|
||||
$category,
|
||||
['forum_unread' => forum_topics_unread($category['forum_id'], $userId)],
|
||||
forum_latest_post($category['forum_id'], $userId)
|
||||
);
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
function forum_get_breadcrumbs(
|
||||
int $forumId,
|
||||
string $linkFormat = '/forum/forum.php?f=%d',
|
||||
string $rootFormat = '/forum/#f%d',
|
||||
array $indexLink = ['Forums' => '/forum/']
|
||||
): array {
|
||||
$breadcrumbs = [];
|
||||
$getBreadcrumb = \Misuzu\DB::prepare('
|
||||
SELECT `forum_id`, `forum_name`, `forum_type`, `forum_parent`
|
||||
FROM `msz_forum_categories`
|
||||
WHERE `forum_id` = :forum_id
|
||||
');
|
||||
|
||||
while($forumId > 0) {
|
||||
$getBreadcrumb->bind('forum_id', $forumId);
|
||||
$breadcrumb = $getBreadcrumb->fetch();
|
||||
|
||||
if(empty($breadcrumb)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$breadcrumbs[$breadcrumb['forum_name']] = sprintf(
|
||||
$breadcrumb['forum_parent'] === MSZ_FORUM_ROOT
|
||||
&& $breadcrumb['forum_type'] === MSZ_FORUM_TYPE_CATEGORY
|
||||
? $rootFormat
|
||||
: $linkFormat,
|
||||
$breadcrumb['forum_id']
|
||||
);
|
||||
$forumId = $breadcrumb['forum_parent'];
|
||||
}
|
||||
|
||||
return array_reverse($breadcrumbs + $indexLink);
|
||||
}
|
||||
|
||||
function forum_get_colour(int $forumId): int {
|
||||
$getColours = \Misuzu\DB::prepare('
|
||||
SELECT `forum_id`, `forum_parent`, `forum_colour`
|
||||
FROM `msz_forum_categories`
|
||||
WHERE `forum_id` = :forum_id
|
||||
');
|
||||
|
||||
while($forumId > 0) {
|
||||
$getColours->bind('forum_id', $forumId);
|
||||
$colourInfo = $getColours->fetch();
|
||||
|
||||
if(empty($colourInfo)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if(!empty($colourInfo['forum_colour'])) {
|
||||
return $colourInfo['forum_colour'];
|
||||
}
|
||||
|
||||
$forumId = $colourInfo['forum_parent'];
|
||||
}
|
||||
|
||||
return 0x40000000;
|
||||
}
|
||||
|
||||
function forum_increment_clicks(int $forumId): void {
|
||||
$incrementLinkClicks = \Misuzu\DB::prepare(sprintf('
|
||||
UPDATE `msz_forum_categories`
|
||||
SET `forum_link_clicks` = `forum_link_clicks` + 1
|
||||
WHERE `forum_id` = :forum_id
|
||||
AND `forum_type` = %d
|
||||
AND `forum_link_clicks` IS NOT NULL
|
||||
', MSZ_FORUM_TYPE_LINK));
|
||||
$incrementLinkClicks->bind('forum_id', $forumId);
|
||||
$incrementLinkClicks->execute();
|
||||
}
|
||||
|
||||
function forum_get_parent_id(int $forumId): int {
|
||||
if($forumId < 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static $memoized = [];
|
||||
|
||||
if(array_key_exists($forumId, $memoized)) {
|
||||
return $memoized[$forumId];
|
||||
}
|
||||
|
||||
$getParent = \Misuzu\DB::prepare('
|
||||
SELECT `forum_parent`
|
||||
FROM `msz_forum_categories`
|
||||
WHERE `forum_id` = :forum_id
|
||||
');
|
||||
$getParent->bind('forum_id', $forumId);
|
||||
|
||||
return (int)$getParent->fetchColumn();
|
||||
}
|
||||
|
||||
function forum_get_child_ids(int $forumId): array {
|
||||
if($forumId < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
static $memoized = [];
|
||||
|
||||
if(array_key_exists($forumId, $memoized)) {
|
||||
return $memoized[$forumId];
|
||||
}
|
||||
|
||||
$getChildren = \Misuzu\DB::prepare('
|
||||
SELECT `forum_id`
|
||||
FROM `msz_forum_categories`
|
||||
WHERE `forum_parent` = :forum_id
|
||||
');
|
||||
$getChildren->bind('forum_id', $forumId);
|
||||
$children = $getChildren->fetchAll();
|
||||
|
||||
return $memoized[$forumId] = array_column($children, 'forum_id');
|
||||
}
|
||||
|
||||
function forum_topics_unread(int $forumId, int $userId): int {
|
||||
if($userId < 1 || $forumId < 1) {
|
||||
return 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_latest_post(int $forumId, int $userId): array {
|
||||
if($forumId < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
static $memoized = [];
|
||||
$memoId = "{$forumId}-{$userId}";
|
||||
|
||||
if(array_key_exists($memoId, $memoized)) {
|
||||
return $memoized[$memoId];
|
||||
}
|
||||
|
||||
if(!forum_perms_check_user(MSZ_FORUM_PERMS_GENERAL, $forumId, $userId, MSZ_FORUM_PERM_SET_READ)) {
|
||||
return $memoized[$memoId] = [];
|
||||
}
|
||||
|
||||
$getLastPost = \Misuzu\DB::prepare('
|
||||
SELECT
|
||||
p.`post_id` AS `recent_post_id`, t.`topic_id` AS `recent_topic_id`,
|
||||
t.`topic_title` AS `recent_topic_title`, t.`topic_bumped` AS `recent_topic_bumped`,
|
||||
p.`post_created` AS `recent_post_created`,
|
||||
u.`user_id` AS `recent_post_user_id`,
|
||||
u.`username` AS `recent_post_username`,
|
||||
COALESCE(u.`user_colour`, r.`role_colour`) AS `recent_post_user_colour`,
|
||||
UNIX_TIMESTAMP(p.`post_created`) AS `post_created_unix`
|
||||
FROM `msz_forum_posts` AS p
|
||||
LEFT JOIN `msz_forum_topics` AS t
|
||||
ON t.`topic_id` = p.`topic_id`
|
||||
LEFT JOIN `msz_users` AS u
|
||||
ON u.`user_id` = p.`user_id`
|
||||
LEFT JOIN `msz_roles` AS r
|
||||
ON r.`role_id` = u.`display_role`
|
||||
WHERE p.`forum_id` = :forum_id
|
||||
AND p.`post_deleted` IS NULL
|
||||
ORDER BY p.`post_id` DESC
|
||||
');
|
||||
$getLastPost->bind('forum_id', $forumId);
|
||||
$currentLast = $getLastPost->fetch();
|
||||
|
||||
$children = forum_get_child_ids($forumId);
|
||||
|
||||
foreach($children as $child) {
|
||||
$lastPost = forum_latest_post($child, $userId);
|
||||
|
||||
if(($currentLast['post_created_unix'] ?? 0) < ($lastPost['post_created_unix'] ?? 0)) {
|
||||
$currentLast = $lastPost;
|
||||
}
|
||||
}
|
||||
|
||||
return $memoized[$memoId] = $currentLast;
|
||||
}
|
||||
|
||||
function forum_get_children(int $parentId, int $userId): array {
|
||||
$getListing = \Misuzu\DB::prepare(sprintf(
|
||||
'
|
||||
SELECT
|
||||
:user_id AS `target_user_id`,
|
||||
f.`forum_id`, f.`forum_name`, f.`forum_description`, f.`forum_type`, f.`forum_icon`,
|
||||
f.`forum_link`, f.`forum_link_clicks`, f.`forum_archived`, f.`forum_colour`,
|
||||
f.`forum_count_topics`, f.`forum_count_posts`
|
||||
FROM `msz_forum_categories` AS f
|
||||
WHERE f.`forum_parent` = :parent_id
|
||||
AND f.`forum_hidden` = 0
|
||||
AND (
|
||||
(f.`forum_parent` = %1$d AND f.`forum_type` != %2$d)
|
||||
OR f.`forum_parent` != %1$d
|
||||
)
|
||||
GROUP BY f.`forum_id`
|
||||
ORDER BY f.`forum_order`
|
||||
',
|
||||
MSZ_FORUM_ROOT,
|
||||
MSZ_FORUM_TYPE_CATEGORY
|
||||
));
|
||||
|
||||
$getListing->bind('user_id', $userId);
|
||||
$getListing->bind('parent_id', $parentId);
|
||||
|
||||
$listing = $getListing->fetchAll();
|
||||
|
||||
foreach($listing as $key => $forum) {
|
||||
$listing[$key]['forum_permissions'] = $perms = forum_perms_get_user($forum['forum_id'], $userId)[MSZ_FORUM_PERMS_GENERAL];
|
||||
|
||||
if(!perms_check($perms, MSZ_FORUM_PERM_SET_READ)) {
|
||||
unset($listing[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$listing[$key] = array_merge(
|
||||
$forum,
|
||||
['forum_unread' => forum_topics_unread($forum['forum_id'], $userId)],
|
||||
forum_latest_post($forum['forum_id'], $userId)
|
||||
);
|
||||
}
|
||||
|
||||
return $listing;
|
||||
}
|
||||
|
||||
function forum_timeout(int $forumId, int $userId): int {
|
||||
$checkTimeout = \Misuzu\DB::prepare('
|
||||
SELECT TIMESTAMPDIFF(SECOND, COALESCE(MAX(`post_created`), NOW() - INTERVAL 1 YEAR), NOW())
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `forum_id` = :forum_id
|
||||
AND `user_id` = :user_id
|
||||
');
|
||||
$checkTimeout->bind('forum_id', $forumId);
|
||||
$checkTimeout->bind('user_id', $userId);
|
||||
|
||||
return (int)$checkTimeout->fetchColumn();
|
||||
}
|
||||
|
||||
// $forumId == null marks all forums as read
|
||||
function forum_mark_read(?int $forumId, int $userId): void {
|
||||
if(($forumId !== null && $forumId < 1) || $userId < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entireForum = $forumId === null;
|
||||
|
||||
if(!$entireForum) {
|
||||
$children = forum_get_child_ids($forumId);
|
||||
|
||||
foreach($children as $child) {
|
||||
forum_mark_read($child, $userId);
|
||||
}
|
||||
}
|
||||
|
||||
$doMark = \Misuzu\DB::prepare(sprintf(
|
||||
'
|
||||
INSERT INTO `msz_forum_topics_track`
|
||||
(`user_id`, `topic_id`, `forum_id`, `track_last_read`)
|
||||
SELECT u.`user_id`, t.`topic_id`, t.`forum_id`, NOW()
|
||||
FROM `msz_forum_topics` AS t
|
||||
LEFT JOIN `msz_users` AS u
|
||||
ON u.`user_id` = :user
|
||||
WHERE t.`topic_deleted` IS NULL
|
||||
AND t.`topic_bumped` >= NOW() - INTERVAL 1 MONTH
|
||||
%1$s
|
||||
GROUP BY t.`topic_id`
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`track_last_read` = NOW()
|
||||
',
|
||||
$entireForum ? '' : 'AND t.`forum_id` = :forum'
|
||||
));
|
||||
$doMark->bind('user', $userId);
|
||||
|
||||
if(!$entireForum) {
|
||||
$doMark->bind('forum', $forumId);
|
||||
}
|
||||
|
||||
$doMark->execute();
|
||||
}
|
||||
|
||||
function forum_posting_info(int $userId): array {
|
||||
$getPostingInfo = \Misuzu\DB::prepare('
|
||||
SELECT
|
||||
u.`user_country`, u.`user_created`,
|
||||
(
|
||||
SELECT COUNT(`post_id`)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `user_id` = u.`user_id`
|
||||
AND `post_deleted` IS NULL
|
||||
) AS `user_forum_posts`,
|
||||
(
|
||||
SELECT `post_parse`
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `user_id` = u.`user_id`
|
||||
AND `post_deleted` IS NULL
|
||||
ORDER BY `post_id` DESC
|
||||
LIMIT 1
|
||||
) AS `user_post_parse`
|
||||
FROM `msz_users` as u
|
||||
WHERE `user_id` = :user_id
|
||||
');
|
||||
$getPostingInfo->bind('user_id', $userId);
|
||||
return $getPostingInfo->fetch();
|
||||
}
|
||||
|
||||
function forum_count_increase(int $forumId, bool $topic = false): void {
|
||||
$increaseCount = \Misuzu\DB::prepare(sprintf(
|
||||
'
|
||||
UPDATE `msz_forum_categories`
|
||||
SET `forum_count_posts` = `forum_count_posts` + 1
|
||||
%s
|
||||
WHERE `forum_id` = :forum
|
||||
',
|
||||
$topic ? ',`forum_count_topics` = `forum_count_topics` + 1' : ''
|
||||
));
|
||||
$increaseCount->bind('forum', $forumId);
|
||||
$increaseCount->execute();
|
||||
}
|
||||
|
||||
function forum_count_synchronise(int $forumId = MSZ_FORUM_ROOT, bool $save = true): array {
|
||||
static $getChildren = null;
|
||||
static $getCounts = null;
|
||||
static $setCounts = null;
|
||||
|
||||
if(is_null($getChildren)) {
|
||||
$getChildren = \Misuzu\DB::prepare('
|
||||
SELECT `forum_id`, `forum_parent`
|
||||
FROM `msz_forum_categories`
|
||||
WHERE `forum_parent` = :parent
|
||||
');
|
||||
}
|
||||
|
||||
if(is_null($getCounts)) {
|
||||
$getCounts = \Misuzu\DB::prepare('
|
||||
SELECT :forum as `target_forum_id`,
|
||||
(
|
||||
SELECT COUNT(`topic_id`)
|
||||
FROM `msz_forum_topics`
|
||||
WHERE `forum_id` = `target_forum_id`
|
||||
AND `topic_deleted` IS NULL
|
||||
) AS `count_topics`,
|
||||
(
|
||||
SELECT COUNT(`post_id`)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `forum_id` = `target_forum_id`
|
||||
AND `post_deleted` IS NULL
|
||||
) AS `count_posts`
|
||||
');
|
||||
}
|
||||
|
||||
if($save && is_null($setCounts)) {
|
||||
$setCounts = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_forum_categories`
|
||||
SET `forum_count_topics` = :topics,
|
||||
`forum_count_posts` = :posts
|
||||
WHERE `forum_id` = :forum_id
|
||||
');
|
||||
}
|
||||
|
||||
$getChildren->bind('parent', $forumId);
|
||||
$children = $getChildren->fetchAll();
|
||||
|
||||
$topics = 0;
|
||||
$posts = 0;
|
||||
|
||||
foreach($children as $child) {
|
||||
$childCount = forum_count_synchronise($child['forum_id'], $save);
|
||||
$topics += $childCount['topics'];
|
||||
$posts += $childCount['posts'];
|
||||
}
|
||||
|
||||
$getCounts->bind('forum', $forumId);
|
||||
$counts = $getCounts->fetch();
|
||||
$topics += $counts['count_topics'];
|
||||
$posts += $counts['count_posts'];
|
||||
|
||||
if($forumId > 0 && $save) {
|
||||
$setCounts->bind('forum_id', $forumId);
|
||||
$setCounts->bind('topics', $topics);
|
||||
$setCounts->bind('posts', $posts);
|
||||
$setCounts->execute();
|
||||
}
|
||||
|
||||
return compact('topics', 'posts');
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
<?php
|
||||
define('MSZ_FORUM_LEADERBOARD_START_YEAR', 2018);
|
||||
define('MSZ_FORUM_LEADERBOARD_START_MONTH', 12);
|
||||
define('MSZ_FORUM_LEADERBOARD_CATEGORY_ALL', 0);
|
||||
|
||||
function forum_leaderboard_year_valid(?int $year): bool {
|
||||
return !is_null($year) && $year >= MSZ_FORUM_LEADERBOARD_START_YEAR && $year <= date('Y');
|
||||
}
|
||||
|
||||
function forum_leaderboard_month_valid(?int $year, ?int $month): bool {
|
||||
if(is_null($month) || !forum_leaderboard_year_valid($year) || $month < 1 || $month > 12) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$combo = sprintf('%04d%02d', $year, $month);
|
||||
$start = sprintf('%04d%02d', MSZ_FORUM_LEADERBOARD_START_YEAR, MSZ_FORUM_LEADERBOARD_START_MONTH);
|
||||
$current = date('Ym');
|
||||
|
||||
return $combo >= $start && $combo <= $current;
|
||||
}
|
||||
|
||||
function forum_leaderboard_categories(): array {
|
||||
$categories = [
|
||||
MSZ_FORUM_LEADERBOARD_CATEGORY_ALL => 'All Time',
|
||||
];
|
||||
|
||||
$currentYear = date('Y');
|
||||
$currentMonth = date('m');
|
||||
|
||||
for($i = $currentYear; $i >= MSZ_FORUM_LEADERBOARD_START_YEAR; $i--) {
|
||||
$categories[$i] = sprintf('Leaderboard %d', $i);
|
||||
}
|
||||
|
||||
for($i = $currentYear, $j = $currentMonth;;) {
|
||||
$categories[sprintf('%d%02d', $i, $j)] = sprintf('Leaderboard %d-%02d', $i, $j);
|
||||
|
||||
if($j <= 1) {
|
||||
$i--; $j = 12;
|
||||
} else $j--;
|
||||
|
||||
if($i <= MSZ_FORUM_LEADERBOARD_START_YEAR && $j < MSZ_FORUM_LEADERBOARD_START_MONTH)
|
||||
break;
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
function forum_leaderboard_listing(
|
||||
?int $year = null,
|
||||
?int $month = null,
|
||||
array $unrankedForums = [],
|
||||
array $unrankedTopics = []
|
||||
): array {
|
||||
$hasYear = forum_leaderboard_year_valid($year);
|
||||
$hasMonth = $hasYear && forum_leaderboard_month_valid($year, $month);
|
||||
$unrankedForums = implode(',', $unrankedForums);
|
||||
$unrankedTopics = implode(',', $unrankedTopics);
|
||||
|
||||
$rawLeaderboard = \Misuzu\DB::query(sprintf(
|
||||
'
|
||||
SELECT
|
||||
u.`user_id`, u.`username`,
|
||||
COUNT(fp.`post_id`) as `posts`
|
||||
FROM `msz_users` AS u
|
||||
INNER JOIN `msz_forum_posts` AS fp
|
||||
ON fp.`user_id` = u.`user_id`
|
||||
WHERE fp.`post_deleted` IS NULL
|
||||
%s %s %s
|
||||
GROUP BY u.`user_id`
|
||||
HAVING `posts` > 0
|
||||
ORDER BY `posts` DESC
|
||||
',
|
||||
$unrankedForums ? sprintf('AND fp.`forum_id` NOT IN (%s)', $unrankedForums) : '',
|
||||
$unrankedTopics ? sprintf('AND fp.`topic_id` NOT IN (%s)', $unrankedTopics) : '',
|
||||
!$hasYear ? '' : sprintf(
|
||||
'AND DATE(fp.`post_created`) BETWEEN \'%1$04d-%2$02d-01\' AND \'%1$04d-%3$02d-31\'',
|
||||
$year,
|
||||
$hasMonth ? $month : 1,
|
||||
$hasMonth ? $month : 12
|
||||
)
|
||||
))->fetchAll();
|
||||
|
||||
$leaderboard = [];
|
||||
$ranking = 0;
|
||||
$lastPosts = null;
|
||||
|
||||
foreach($rawLeaderboard as $entry) {
|
||||
if(is_null($lastPosts) || $lastPosts > $entry['posts']) {
|
||||
$ranking++;
|
||||
$lastPosts = $entry['posts'];
|
||||
}
|
||||
|
||||
$entry['rank'] = $ranking;
|
||||
$leaderboard[] = $entry;
|
||||
}
|
||||
|
||||
return $leaderboard;
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -1,200 +0,0 @@
|
|||
<?php
|
||||
function forum_poll_get(int $poll): array {
|
||||
if($poll < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$getPoll = \Misuzu\DB::prepare("
|
||||
SELECT fp.`poll_id`, fp.`poll_max_votes`, fp.`poll_expires`, fp.`poll_preview_results`, fp.`poll_change_vote`,
|
||||
(fp.`poll_expires` < CURRENT_TIMESTAMP) AS `poll_expired`,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM `msz_forum_polls_answers`
|
||||
WHERE `poll_id` = fp.`poll_id`
|
||||
) AS `poll_votes`
|
||||
FROM `msz_forum_polls` AS fp
|
||||
WHERE fp.`poll_id` = :poll
|
||||
");
|
||||
$getPoll->bind('poll', $poll);
|
||||
return $getPoll->fetch();
|
||||
}
|
||||
|
||||
function forum_poll_create(int $maxVotes = 1): int {
|
||||
if($maxVotes < 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
$createPoll = \Misuzu\DB::prepare("
|
||||
INSERT INTO `msz_forum_polls`
|
||||
(`poll_max_votes`)
|
||||
VALUES
|
||||
(:max_votes)
|
||||
");
|
||||
$createPoll->bind('max_votes', $maxVotes);
|
||||
return $createPoll->execute() ? \Misuzu\DB::lastId() : -1;
|
||||
}
|
||||
|
||||
function forum_poll_get_options(int $poll): array {
|
||||
if($poll < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
static $polls = [];
|
||||
|
||||
if(array_key_exists($poll, $polls)) {
|
||||
return $polls[$poll];
|
||||
}
|
||||
|
||||
$getOptions = \Misuzu\DB::prepare('
|
||||
SELECT `option_id`, `option_text`,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM `msz_forum_polls_answers`
|
||||
WHERE `option_id` = fpo.`option_id`
|
||||
) AS `option_votes`
|
||||
FROM `msz_forum_polls_options` AS fpo
|
||||
WHERE `poll_id` = :poll
|
||||
');
|
||||
$getOptions->bind('poll', $poll);
|
||||
|
||||
return $polls[$poll] = $getOptions->fetchAll();
|
||||
}
|
||||
|
||||
function forum_poll_get_user_answers(int $poll, int $user): array {
|
||||
if($poll < 1 || $user < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$getAnswers = \Misuzu\DB::prepare("
|
||||
SELECT `option_id`
|
||||
FROM `msz_forum_polls_answers`
|
||||
WHERE `poll_id` = :poll
|
||||
AND `user_id` = :user
|
||||
");
|
||||
$getAnswers->bind('poll', $poll);
|
||||
$getAnswers->bind('user', $user);
|
||||
return array_column($getAnswers->fetchAll(), 'option_id');
|
||||
}
|
||||
|
||||
function forum_poll_reset_answers(int $poll): void {
|
||||
if($poll < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resetAnswers = \Misuzu\DB::prepare("
|
||||
DELETE FROM `msz_forum_polls_answers`
|
||||
WHERE `poll_id` = :poll
|
||||
");
|
||||
$resetAnswers->bind('poll', $poll);
|
||||
$resetAnswers->execute();
|
||||
}
|
||||
|
||||
function forum_poll_option_add(int $poll, string $text): int {
|
||||
if($poll < 1 || empty($text) || strlen($text) > 0xFF) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
$addOption = \Misuzu\DB::prepare("
|
||||
INSERT INTO `msz_forum_polls_options`
|
||||
(`poll_id`, `option_text`)
|
||||
VALUES
|
||||
(:poll, :text)
|
||||
");
|
||||
$addOption->bind('poll', $poll);
|
||||
$addOption->bind('text', $text);
|
||||
return $addOption->execute() ? \Misuzu\DB::lastId() : -1;
|
||||
}
|
||||
|
||||
function forum_poll_option_remove(int $option): void {
|
||||
if($option < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$removeOption = \Misuzu\DB::prepare("
|
||||
DELETE FROM `msz_forum_polls_options`
|
||||
WHERE `option_id` = :option
|
||||
");
|
||||
$removeOption->bind('option', $option);
|
||||
$removeOption->execute();
|
||||
}
|
||||
|
||||
function forum_poll_vote_remove(int $user, int $poll): void {
|
||||
if($user < 1 || $poll < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$purgeVote = \Misuzu\DB::prepare("
|
||||
DELETE FROM `msz_forum_polls_answers`
|
||||
WHERE `user_id` = :user
|
||||
AND `poll_id` = :poll
|
||||
");
|
||||
$purgeVote->bind('user', $user);
|
||||
$purgeVote->bind('poll', $poll);
|
||||
$purgeVote->execute();
|
||||
}
|
||||
|
||||
function forum_poll_vote_cast(int $user, int $poll, int $option): void {
|
||||
if($user < 1 || $poll < 1 || $option < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$castVote = \Misuzu\DB::prepare("
|
||||
INSERT INTO `msz_forum_polls_answers`
|
||||
(`user_id`, `poll_id`, `option_id`)
|
||||
VALUES
|
||||
(:user, :poll, :option)
|
||||
");
|
||||
$castVote->bind('user', $user);
|
||||
$castVote->bind('poll', $poll);
|
||||
$castVote->bind('option', $option);
|
||||
$castVote->execute();
|
||||
}
|
||||
|
||||
function forum_poll_validate_option(int $poll, int $option): bool {
|
||||
if($poll < 1 || $option < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$checkVote = \Misuzu\DB::prepare("
|
||||
SELECT COUNT(`option_id`) > 0
|
||||
FROM `msz_forum_polls_options`
|
||||
WHERE `poll_id` = :poll
|
||||
AND `option_id` = :option
|
||||
");
|
||||
$checkVote->bind('poll', $poll);
|
||||
$checkVote->bind('option', $option);
|
||||
|
||||
return (bool)$checkVote->fetchColumn();
|
||||
}
|
||||
|
||||
function forum_poll_has_voted(int $user, int $poll): bool {
|
||||
if($user < 1 || $poll < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$getAnswers = \Misuzu\DB::prepare("
|
||||
SELECT COUNT(`user_id`) > 0
|
||||
FROM `msz_forum_polls_answers`
|
||||
WHERE `poll_id` = :poll
|
||||
AND `user_id` = :user
|
||||
");
|
||||
$getAnswers->bind('poll', $poll);
|
||||
$getAnswers->bind('user', $user);
|
||||
|
||||
return (bool)$getAnswers->fetchColumn();
|
||||
}
|
||||
|
||||
function forum_poll_get_topic(int $poll): array {
|
||||
if($poll < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$getTopic = \Misuzu\DB::prepare("
|
||||
SELECT `forum_id`, `topic_id`, `topic_locked`
|
||||
FROM `msz_forum_topics`
|
||||
WHERE `poll_id` = :poll
|
||||
");
|
||||
$getTopic->bind('poll', $poll);
|
||||
|
||||
return $getTopic->fetch();
|
||||
}
|
|
@ -1,360 +0,0 @@
|
|||
<?php
|
||||
define('MSZ_FORUM_POSTS_PER_PAGE', 10);
|
||||
|
||||
function forum_post_create(
|
||||
int $topicId,
|
||||
int $forumId,
|
||||
int $userId,
|
||||
string $ipAddress,
|
||||
string $text,
|
||||
int $parser = \Misuzu\Parsers\Parser::PLAIN,
|
||||
bool $displaySignature = true
|
||||
): int {
|
||||
$createPost = \Misuzu\DB::prepare('
|
||||
INSERT INTO `msz_forum_posts`
|
||||
(`topic_id`, `forum_id`, `user_id`, `post_ip`, `post_text`, `post_parse`, `post_display_signature`)
|
||||
VALUES
|
||||
(:topic_id, :forum_id, :user_id, INET6_ATON(:post_ip), :post_text, :post_parse, :post_display_signature)
|
||||
');
|
||||
$createPost->bind('topic_id', $topicId);
|
||||
$createPost->bind('forum_id', $forumId);
|
||||
$createPost->bind('user_id', $userId);
|
||||
$createPost->bind('post_ip', $ipAddress);
|
||||
$createPost->bind('post_text', $text);
|
||||
$createPost->bind('post_parse', $parser);
|
||||
$createPost->bind('post_display_signature', $displaySignature ? 1 : 0);
|
||||
|
||||
return $createPost->execute() ? \Misuzu\DB::lastId() : 0;
|
||||
}
|
||||
|
||||
function forum_post_update(
|
||||
int $postId,
|
||||
string $ipAddress,
|
||||
string $text,
|
||||
int $parser = \Misuzu\Parsers\Parser::PLAIN,
|
||||
bool $displaySignature = true,
|
||||
bool $bumpUpdate = true
|
||||
): bool {
|
||||
if($postId < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$updatePost = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_forum_posts`
|
||||
SET `post_ip` = INET6_ATON(:post_ip),
|
||||
`post_text` = :post_text,
|
||||
`post_parse` = :post_parse,
|
||||
`post_display_signature` = :post_display_signature,
|
||||
`post_edited` = IF(:bump, NOW(), `post_edited`)
|
||||
WHERE `post_id` = :post_id
|
||||
');
|
||||
$updatePost->bind('post_id', $postId);
|
||||
$updatePost->bind('post_ip', $ipAddress);
|
||||
$updatePost->bind('post_text', $text);
|
||||
$updatePost->bind('post_parse', $parser);
|
||||
$updatePost->bind('post_display_signature', $displaySignature ? 1 : 0);
|
||||
$updatePost->bind('bump', $bumpUpdate ? 1 : 0);
|
||||
|
||||
return $updatePost->execute();
|
||||
}
|
||||
|
||||
function forum_post_find(int $postId, int $userId): array {
|
||||
$getPostInfo = \Misuzu\DB::prepare(sprintf(
|
||||
'
|
||||
SELECT
|
||||
p.`post_id`, p.`topic_id`,
|
||||
(
|
||||
SELECT COUNT(`post_id`)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = p.`topic_id`
|
||||
AND `post_id` < p.`post_id`
|
||||
AND `post_deleted` IS NULL
|
||||
ORDER BY `post_id`
|
||||
) as `preceeding_post_count`,
|
||||
(
|
||||
SELECT COUNT(`post_id`)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = p.`topic_id`
|
||||
AND `post_id` < p.`post_id`
|
||||
AND `post_deleted` IS NOT NULL
|
||||
ORDER BY `post_id`
|
||||
) as `preceeding_post_deleted_count`
|
||||
FROM `msz_forum_posts` AS p
|
||||
WHERE p.`post_id` = :post_id
|
||||
'));
|
||||
$getPostInfo->bind('post_id', $postId);
|
||||
return $getPostInfo->fetch();
|
||||
}
|
||||
|
||||
function forum_post_get(int $postId, bool $allowDeleted = false): array {
|
||||
$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();
|
||||
}
|
|
@ -1,709 +0,0 @@
|
|||
<?php
|
||||
define('MSZ_TOPIC_TYPE_DISCUSSION', 0);
|
||||
define('MSZ_TOPIC_TYPE_STICKY', 1);
|
||||
define('MSZ_TOPIC_TYPE_ANNOUNCEMENT', 2);
|
||||
define('MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT', 3);
|
||||
define('MSZ_TOPIC_TYPES', [
|
||||
MSZ_TOPIC_TYPE_DISCUSSION,
|
||||
MSZ_TOPIC_TYPE_STICKY,
|
||||
MSZ_TOPIC_TYPE_ANNOUNCEMENT,
|
||||
MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT,
|
||||
]);
|
||||
|
||||
define('MSZ_TOPIC_TYPE_ORDER', [ // in which order to display topics, only add types here that should appear above others
|
||||
MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT,
|
||||
MSZ_TOPIC_TYPE_ANNOUNCEMENT,
|
||||
MSZ_TOPIC_TYPE_STICKY,
|
||||
]);
|
||||
|
||||
function forum_topic_is_valid_type(int $type): bool {
|
||||
return in_array($type, MSZ_TOPIC_TYPES, true);
|
||||
}
|
||||
|
||||
function forum_topic_create(
|
||||
int $forumId,
|
||||
int $userId,
|
||||
string $title,
|
||||
int $type = MSZ_TOPIC_TYPE_DISCUSSION
|
||||
): int {
|
||||
if(empty($title) || !forum_topic_is_valid_type($type)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$createTopic = \Misuzu\DB::prepare('
|
||||
INSERT INTO `msz_forum_topics`
|
||||
(`forum_id`, `user_id`, `topic_title`, `topic_type`)
|
||||
VALUES
|
||||
(:forum_id, :user_id, :topic_title, :topic_type)
|
||||
');
|
||||
$createTopic->bind('forum_id', $forumId);
|
||||
$createTopic->bind('user_id', $userId);
|
||||
$createTopic->bind('topic_title', $title);
|
||||
$createTopic->bind('topic_type', $type);
|
||||
|
||||
return $createTopic->execute() ? \Misuzu\DB::lastId() : 0;
|
||||
}
|
||||
|
||||
function forum_topic_update(int $topicId, ?string $title, ?int $type = null): bool {
|
||||
if($topicId < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// make sure it's null and not some other kinda empty
|
||||
if(empty($title)) {
|
||||
$title = null;
|
||||
}
|
||||
|
||||
if($type !== null && !forum_topic_is_valid_type($type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$updateTopic = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_forum_topics`
|
||||
SET `topic_title` = COALESCE(:topic_title, `topic_title`),
|
||||
`topic_type` = COALESCE(:topic_type, `topic_type`)
|
||||
WHERE `topic_id` = :topic_id
|
||||
');
|
||||
$updateTopic->bind('topic_id', $topicId);
|
||||
$updateTopic->bind('topic_title', $title);
|
||||
$updateTopic->bind('topic_type', $type);
|
||||
|
||||
return $updateTopic->execute();
|
||||
}
|
||||
|
||||
function forum_topic_get(int $topicId, bool $allowDeleted = false): array {
|
||||
$getTopic = \Misuzu\DB::prepare(sprintf(
|
||||
'
|
||||
SELECT
|
||||
t.`topic_id`, t.`forum_id`, t.`topic_title`, t.`topic_type`, t.`topic_locked`, t.`topic_created`,
|
||||
f.`forum_archived` AS `topic_archived`, t.`topic_deleted`, t.`topic_bumped`, f.`forum_type`,
|
||||
tp.`poll_id`, tp.`poll_max_votes`, tp.`poll_expires`, tp.`poll_preview_results`, tp.`poll_change_vote`,
|
||||
(tp.`poll_expires` < CURRENT_TIMESTAMP) AS `poll_expired`,
|
||||
fp.`topic_id` AS `author_post_id`, fp.`user_id` AS `author_user_id`,
|
||||
(
|
||||
SELECT COUNT(`post_id`)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
AND `post_deleted` IS NULL
|
||||
) AS `topic_count_posts`,
|
||||
(
|
||||
SELECT COUNT(`post_id`)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
AND `post_deleted` IS NOT NULL
|
||||
) AS `topic_count_posts_deleted`,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM `msz_forum_polls_answers`
|
||||
WHERE `poll_id` = tp.`poll_id`
|
||||
) AS `poll_votes`
|
||||
FROM `msz_forum_topics` AS t
|
||||
LEFT JOIN `msz_forum_categories` AS f
|
||||
ON f.`forum_id` = t.`forum_id`
|
||||
LEFT JOIN `msz_forum_posts` AS fp
|
||||
ON fp.`post_id` = (
|
||||
SELECT MIN(`post_id`)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
)
|
||||
LEFT JOIN `msz_forum_polls` AS tp
|
||||
ON tp.`poll_id` = t.`poll_id`
|
||||
WHERE t.`topic_id` = :topic_id
|
||||
%s
|
||||
',
|
||||
$allowDeleted ? '' : 'AND t.`topic_deleted` IS NULL'
|
||||
));
|
||||
$getTopic->bind('topic_id', $topicId);
|
||||
return $getTopic->fetch();
|
||||
}
|
||||
|
||||
function forum_topic_bump(int $topicId): bool {
|
||||
$bumpTopic = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_forum_topics`
|
||||
SET `topic_bumped` = NOW()
|
||||
WHERE `topic_id` = :topic_id
|
||||
AND `topic_deleted` IS NULL
|
||||
');
|
||||
$bumpTopic->bind('topic_id', $topicId);
|
||||
return $bumpTopic->execute();
|
||||
}
|
||||
|
||||
function forum_topic_views_increment(int $topicId): void {
|
||||
if($topicId < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bumpViews = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_forum_topics`
|
||||
SET `topic_count_views` = `topic_count_views` + 1
|
||||
WHERE `topic_id` = :topic_id
|
||||
');
|
||||
$bumpViews->bind('topic_id', $topicId);
|
||||
$bumpViews->execute();
|
||||
}
|
||||
|
||||
function forum_topic_mark_read(int $userId, int $topicId, int $forumId): void {
|
||||
if($userId < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// previously a TRIGGER was used to achieve this behaviour,
|
||||
// but those explode when running on a lot of queries (like forum_mark_read() does)
|
||||
// so instead we get to live with this garbage now
|
||||
try {
|
||||
$markAsRead = \Misuzu\DB::prepare('
|
||||
INSERT INTO `msz_forum_topics_track`
|
||||
(`user_id`, `topic_id`, `forum_id`, `track_last_read`)
|
||||
VALUES
|
||||
(:user_id, :topic_id, :forum_id, NOW())
|
||||
');
|
||||
$markAsRead->bind('user_id', $userId);
|
||||
$markAsRead->bind('topic_id', $topicId);
|
||||
$markAsRead->bind('forum_id', $forumId);
|
||||
|
||||
if($markAsRead->execute()) {
|
||||
forum_topic_views_increment($topicId);
|
||||
}
|
||||
} catch(PDOException $ex) {
|
||||
if($ex->getCode() != '23000') {
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
$markAsRead = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_forum_topics_track`
|
||||
SET `track_last_read` = NOW(),
|
||||
`forum_id` = :forum_id
|
||||
WHERE `user_id` = :user_id
|
||||
AND `topic_id` = :topic_id
|
||||
');
|
||||
$markAsRead->bind('user_id', $userId);
|
||||
$markAsRead->bind('topic_id', $topicId);
|
||||
$markAsRead->bind('forum_id', $forumId);
|
||||
$markAsRead->execute();
|
||||
}
|
||||
}
|
||||
|
||||
function forum_topic_listing(
|
||||
int $forumId, int $userId,
|
||||
int $offset = 0, int $take = 0,
|
||||
bool $showDeleted = false, bool $sortByPriority = false
|
||||
): array {
|
||||
$hasPagination = $offset >= 0 && $take > 0;
|
||||
$getTopics = \Misuzu\DB::prepare(sprintf(
|
||||
'
|
||||
SELECT
|
||||
:user_id AS `target_user_id`,
|
||||
t.`topic_id`, t.`topic_title`, t.`topic_locked`, t.`topic_type`, t.`topic_created`,
|
||||
t.`topic_bumped`, t.`topic_deleted`, t.`topic_count_views`, f.`forum_type`,
|
||||
COALESCE(SUM(tp.`topic_priority`), 0) AS `topic_priority`,
|
||||
au.`user_id` AS `author_id`, au.`username` AS `author_name`,
|
||||
COALESCE(au.`user_colour`, ar.`role_colour`) AS `author_colour`,
|
||||
lp.`post_id` AS `response_id`,
|
||||
lp.`post_created` AS `response_created`,
|
||||
lu.`user_id` AS `respondent_id`,
|
||||
lu.`username` AS `respondent_name`,
|
||||
COALESCE(lu.`user_colour`, lr.`role_colour`) AS `respondent_colour`,
|
||||
(
|
||||
SELECT COUNT(`post_id`)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
%5$s
|
||||
) AS `topic_count_posts`,
|
||||
(
|
||||
SELECT CEIL(COUNT(`post_id`) / %6$d)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
%5$s
|
||||
) AS `topic_pages`,
|
||||
(
|
||||
SELECT
|
||||
`target_user_id` > 0
|
||||
AND
|
||||
t.`topic_bumped` > NOW() - INTERVAL 1 MONTH
|
||||
AND (
|
||||
SELECT COUNT(ti.`topic_id`) < 1
|
||||
FROM `msz_forum_topics_track` AS tt
|
||||
RIGHT JOIN `msz_forum_topics` AS ti
|
||||
ON ti.`topic_id` = tt.`topic_id`
|
||||
WHERE ti.`topic_id` = t.`topic_id`
|
||||
AND tt.`user_id` = `target_user_id`
|
||||
AND `track_last_read` >= `topic_bumped`
|
||||
)
|
||||
) AS `topic_unread`,
|
||||
(
|
||||
SELECT COUNT(`post_id`) > 0
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
AND `user_id` = `target_user_id`
|
||||
LIMIT 1
|
||||
) AS `topic_participated`
|
||||
FROM `msz_forum_topics` AS t
|
||||
LEFT JOIN `msz_forum_topics_priority` AS tp
|
||||
ON tp.`topic_id` = t.`topic_id`
|
||||
LEFT JOIN `msz_forum_categories` AS f
|
||||
ON f.`forum_id` = t.`forum_id`
|
||||
LEFT JOIN `msz_users` AS au
|
||||
ON t.`user_id` = au.`user_id`
|
||||
LEFT JOIN `msz_roles` AS ar
|
||||
ON ar.`role_id` = au.`display_role`
|
||||
LEFT JOIN `msz_forum_posts` AS lp
|
||||
ON lp.`post_id` = (
|
||||
SELECT `post_id`
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
%5$s
|
||||
ORDER BY `post_id` DESC
|
||||
LIMIT 1
|
||||
)
|
||||
LEFT JOIN `msz_users` AS lu
|
||||
ON lu.`user_id` = lp.`user_id`
|
||||
LEFT JOIN `msz_roles` AS lr
|
||||
ON lr.`role_id` = lu.`display_role`
|
||||
WHERE (
|
||||
t.`forum_id` = :forum_id
|
||||
OR t.`topic_type` = %3$d
|
||||
)
|
||||
%1$s
|
||||
GROUP BY t.`topic_id`
|
||||
ORDER BY FIELD(t.`topic_type`, %4$s) DESC, %7$s t.`topic_bumped` DESC
|
||||
%2$s
|
||||
',
|
||||
$showDeleted ? '' : 'AND t.`topic_deleted` IS NULL',
|
||||
$hasPagination ? 'LIMIT :offset, :take' : '',
|
||||
MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT,
|
||||
implode(',', array_reverse(MSZ_TOPIC_TYPE_ORDER)),
|
||||
$showDeleted ? '' : 'AND `post_deleted` IS NULL',
|
||||
MSZ_FORUM_POSTS_PER_PAGE,
|
||||
$sortByPriority ? '`topic_priority` DESC,' : ''
|
||||
));
|
||||
$getTopics->bind('forum_id', $forumId);
|
||||
$getTopics->bind('user_id', $userId);
|
||||
|
||||
if($hasPagination) {
|
||||
$getTopics->bind('offset', $offset);
|
||||
$getTopics->bind('take', $take);
|
||||
}
|
||||
|
||||
return $getTopics->fetchAll();
|
||||
}
|
||||
|
||||
function forum_topic_count_user(int $authorId, int $userId, bool $showDeleted = false): int {
|
||||
$getTopics = \Misuzu\DB::prepare(sprintf(
|
||||
'
|
||||
SELECT COUNT(`topic_id`)
|
||||
FROM `msz_forum_topics` AS t
|
||||
WHERE t.`user_id` = :author_id
|
||||
%1$s
|
||||
',
|
||||
$showDeleted ? '' : 'AND t.`topic_deleted` IS NULL'
|
||||
));
|
||||
$getTopics->bind('author_id', $authorId);
|
||||
//$getTopics->bind('user_id', $userId);
|
||||
|
||||
return (int)$getTopics->fetchColumn();
|
||||
}
|
||||
|
||||
// Remove unneccesary stuff from the sql stmt
|
||||
function forum_topic_listing_user(
|
||||
int $authorId,
|
||||
int $userId,
|
||||
int $offset = 0,
|
||||
int $take = 0,
|
||||
bool $showDeleted = false
|
||||
): array {
|
||||
$hasPagination = $offset >= 0 && $take > 0;
|
||||
$getTopics = \Misuzu\DB::prepare(sprintf(
|
||||
'
|
||||
SELECT
|
||||
:user_id AS `target_user_id`,
|
||||
t.`topic_id`, t.`topic_title`, t.`topic_locked`, t.`topic_type`, t.`topic_created`,
|
||||
t.`topic_bumped`, t.`topic_deleted`, t.`topic_count_views`, f.`forum_type`,
|
||||
au.`user_id` AS `author_id`, au.`username` AS `author_name`,
|
||||
COALESCE(au.`user_colour`, ar.`role_colour`) AS `author_colour`,
|
||||
lp.`post_id` AS `response_id`,
|
||||
lp.`post_created` AS `response_created`,
|
||||
lu.`user_id` AS `respondent_id`,
|
||||
lu.`username` AS `respondent_name`,
|
||||
COALESCE(lu.`user_colour`, lr.`role_colour`) AS `respondent_colour`,
|
||||
(
|
||||
SELECT COUNT(`post_id`)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
%5$s
|
||||
) AS `topic_count_posts`,
|
||||
(
|
||||
SELECT CEIL(COUNT(`post_id`) / %6$d)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
%5$s
|
||||
) AS `topic_pages`,
|
||||
(
|
||||
SELECT
|
||||
`target_user_id` > 0
|
||||
AND
|
||||
t.`topic_bumped` > NOW() - INTERVAL 1 MONTH
|
||||
AND (
|
||||
SELECT COUNT(ti.`topic_id`) < 1
|
||||
FROM `msz_forum_topics_track` AS tt
|
||||
RIGHT JOIN `msz_forum_topics` AS ti
|
||||
ON ti.`topic_id` = tt.`topic_id`
|
||||
WHERE ti.`topic_id` = t.`topic_id`
|
||||
AND tt.`user_id` = `target_user_id`
|
||||
AND `track_last_read` >= `topic_bumped`
|
||||
)
|
||||
) AS `topic_unread`,
|
||||
(
|
||||
SELECT COUNT(`post_id`) > 0
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
AND `user_id` = `target_user_id`
|
||||
LIMIT 1
|
||||
) AS `topic_participated`
|
||||
FROM `msz_forum_topics` AS t
|
||||
LEFT JOIN `msz_forum_categories` AS f
|
||||
ON f.`forum_id` = t.`forum_id`
|
||||
LEFT JOIN `msz_users` AS au
|
||||
ON t.`user_id` = au.`user_id`
|
||||
LEFT JOIN `msz_roles` AS ar
|
||||
ON ar.`role_id` = au.`display_role`
|
||||
LEFT JOIN `msz_forum_posts` AS lp
|
||||
ON lp.`post_id` = (
|
||||
SELECT `post_id`
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
%5$s
|
||||
ORDER BY `post_id` DESC
|
||||
LIMIT 1
|
||||
)
|
||||
LEFT JOIN `msz_users` AS lu
|
||||
ON lu.`user_id` = lp.`user_id`
|
||||
LEFT JOIN `msz_roles` AS lr
|
||||
ON lr.`role_id` = lu.`display_role`
|
||||
WHERE au.`user_id` = :author_id
|
||||
%1$s
|
||||
ORDER BY FIELD(t.`topic_type`, %4$s) DESC, t.`topic_bumped` DESC
|
||||
%2$s
|
||||
',
|
||||
$showDeleted ? '' : 'AND t.`topic_deleted` IS NULL',
|
||||
$hasPagination ? 'LIMIT :offset, :take' : '',
|
||||
MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT,
|
||||
implode(',', array_reverse(MSZ_TOPIC_TYPE_ORDER)),
|
||||
$showDeleted ? '' : 'AND `post_deleted` IS NULL',
|
||||
MSZ_FORUM_POSTS_PER_PAGE
|
||||
));
|
||||
$getTopics->bind('author_id', $authorId);
|
||||
$getTopics->bind('user_id', $userId);
|
||||
|
||||
if($hasPagination) {
|
||||
$getTopics->bind('offset', $offset);
|
||||
$getTopics->bind('take', $take);
|
||||
}
|
||||
|
||||
return $getTopics->fetchAll();
|
||||
}
|
||||
|
||||
function forum_topic_listing_search(string $query, int $userId): array {
|
||||
$getTopics = \Misuzu\DB::prepare(sprintf(
|
||||
'
|
||||
SELECT
|
||||
:user_id AS `target_user_id`,
|
||||
t.`topic_id`, t.`topic_title`, t.`topic_locked`, t.`topic_type`, t.`topic_created`,
|
||||
t.`topic_bumped`, t.`topic_deleted`, t.`topic_count_views`, f.`forum_type`,
|
||||
au.`user_id` AS `author_id`, au.`username` AS `author_name`,
|
||||
COALESCE(au.`user_colour`, ar.`role_colour`) AS `author_colour`,
|
||||
lp.`post_id` AS `response_id`,
|
||||
lp.`post_created` AS `response_created`,
|
||||
lu.`user_id` AS `respondent_id`,
|
||||
lu.`username` AS `respondent_name`,
|
||||
COALESCE(lu.`user_colour`, lr.`role_colour`) AS `respondent_colour`,
|
||||
(
|
||||
SELECT COUNT(`post_id`)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
AND `post_deleted` IS NULL
|
||||
) AS `topic_count_posts`,
|
||||
(
|
||||
SELECT CEIL(COUNT(`post_id`) / %2$d)
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
AND `post_deleted` IS NULL
|
||||
) AS `topic_pages`,
|
||||
(
|
||||
SELECT
|
||||
`target_user_id` > 0
|
||||
AND
|
||||
t.`topic_bumped` > NOW() - INTERVAL 1 MONTH
|
||||
AND (
|
||||
SELECT COUNT(ti.`topic_id`) < 1
|
||||
FROM `msz_forum_topics_track` AS tt
|
||||
RIGHT JOIN `msz_forum_topics` AS ti
|
||||
ON ti.`topic_id` = tt.`topic_id`
|
||||
WHERE ti.`topic_id` = t.`topic_id`
|
||||
AND tt.`user_id` = `target_user_id`
|
||||
AND `track_last_read` >= `topic_bumped`
|
||||
)
|
||||
) AS `topic_unread`,
|
||||
(
|
||||
SELECT COUNT(`post_id`) > 0
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
AND `user_id` = `target_user_id`
|
||||
LIMIT 1
|
||||
) AS `topic_participated`
|
||||
FROM `msz_forum_topics` AS t
|
||||
LEFT JOIN `msz_forum_categories` AS f
|
||||
ON f.`forum_id` = t.`forum_id`
|
||||
LEFT JOIN `msz_users` AS au
|
||||
ON t.`user_id` = au.`user_id`
|
||||
LEFT JOIN `msz_roles` AS ar
|
||||
ON ar.`role_id` = au.`display_role`
|
||||
LEFT JOIN `msz_forum_posts` AS lp
|
||||
ON lp.`post_id` = (
|
||||
SELECT `post_id`
|
||||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
AND `post_deleted` IS NULL
|
||||
ORDER BY `post_id` DESC
|
||||
LIMIT 1
|
||||
)
|
||||
LEFT JOIN `msz_users` AS lu
|
||||
ON lu.`user_id` = lp.`user_id`
|
||||
LEFT JOIN `msz_roles` AS lr
|
||||
ON lr.`role_id` = lu.`display_role`
|
||||
WHERE MATCH(`topic_title`)
|
||||
AGAINST (:query IN NATURAL LANGUAGE MODE)
|
||||
AND t.`topic_deleted` IS NULL
|
||||
ORDER BY FIELD(t.`topic_type`, %1$s) DESC, t.`topic_bumped` DESC
|
||||
',
|
||||
implode(',', array_reverse(MSZ_TOPIC_TYPE_ORDER)),
|
||||
MSZ_FORUM_POSTS_PER_PAGE
|
||||
));
|
||||
$getTopics->bind('query', $query);
|
||||
$getTopics->bind('user_id', $userId);
|
||||
|
||||
return $getTopics->fetchAll();
|
||||
}
|
||||
|
||||
function forum_topic_lock(int $topicId): bool {
|
||||
if($topicId < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$markLocked = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_forum_topics`
|
||||
SET `topic_locked` = NOW()
|
||||
WHERE `topic_id` = :topic
|
||||
AND `topic_locked` IS NULL
|
||||
');
|
||||
$markLocked->bind('topic', $topicId);
|
||||
|
||||
return $markLocked->execute();
|
||||
}
|
||||
|
||||
function forum_topic_unlock(int $topicId): bool {
|
||||
if($topicId < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$markUnlocked = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_forum_topics`
|
||||
SET `topic_locked` = NULL
|
||||
WHERE `topic_id` = :topic
|
||||
AND `topic_locked` IS NOT NULL
|
||||
');
|
||||
$markUnlocked->bind('topic', $topicId);
|
||||
|
||||
return $markUnlocked->execute();
|
||||
}
|
||||
|
||||
define('MSZ_E_FORUM_TOPIC_DELETE_OK', 0); // deleting is fine
|
||||
define('MSZ_E_FORUM_TOPIC_DELETE_USER', 1); // invalid user
|
||||
define('MSZ_E_FORUM_TOPIC_DELETE_TOPIC', 2); // topic doesn't exist
|
||||
define('MSZ_E_FORUM_TOPIC_DELETE_DELETED', 3); // topic is already marked as deleted
|
||||
define('MSZ_E_FORUM_TOPIC_DELETE_OWNER', 4); // you may only delete your own topics
|
||||
define('MSZ_E_FORUM_TOPIC_DELETE_OLD', 5); // topic has existed for too long to be deleted
|
||||
define('MSZ_E_FORUM_TOPIC_DELETE_PERM', 6); // you aren't allowed to delete topics
|
||||
define('MSZ_E_FORUM_TOPIC_DELETE_POSTS', 7); // the topic already has replies
|
||||
|
||||
// only allow topics made within a day of posting to be deleted by normal users
|
||||
define('MSZ_FORUM_TOPIC_DELETE_TIME_LIMIT', 60 * 60 * 24);
|
||||
|
||||
// only allow topics with a single post to be deleted, includes soft deleted posts
|
||||
define('MSZ_FORUM_TOPIC_DELETE_POST_LIMIT', 1);
|
||||
|
||||
// set $userId to null for system request, make sure this is NEVER EVER null on user request
|
||||
// $topicId can also be a the return value of forum_topic_get if you already grabbed it once before
|
||||
function forum_topic_can_delete($topicId, ?int $userId = null): int {
|
||||
if($userId !== null && $userId < 1) {
|
||||
return MSZ_E_FORUM_TOPIC_DELETE_USER;
|
||||
}
|
||||
|
||||
if(is_array($topicId)) {
|
||||
$topic = $topicId;
|
||||
} else {
|
||||
$topic = forum_topic_get((int)$topicId, true);
|
||||
}
|
||||
|
||||
if(empty($topic)) {
|
||||
return MSZ_E_FORUM_TOPIC_DELETE_TOPIC;
|
||||
}
|
||||
|
||||
$isSystemReq = $userId === null;
|
||||
$perms = $isSystemReq ? 0 : forum_perms_get_user($topic['forum_id'], $userId)[MSZ_FORUM_PERMS_GENERAL];
|
||||
$canDeleteAny = $isSystemReq ? true : perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST);
|
||||
$canViewPost = $isSystemReq ? true : perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM);
|
||||
$postIsDeleted = !empty($topic['topic_deleted']);
|
||||
|
||||
if(!$canViewPost) {
|
||||
return MSZ_E_FORUM_TOPIC_DELETE_TOPIC;
|
||||
}
|
||||
|
||||
if($postIsDeleted) {
|
||||
return $canDeleteAny ? MSZ_E_FORUM_TOPIC_DELETE_DELETED : MSZ_E_FORUM_TOPIC_DELETE_TOPIC;
|
||||
}
|
||||
|
||||
if($isSystemReq) {
|
||||
return MSZ_E_FORUM_TOPIC_DELETE_OK;
|
||||
}
|
||||
|
||||
if(!$canDeleteAny) {
|
||||
if(!perms_check($perms, MSZ_FORUM_PERM_DELETE_POST)) {
|
||||
return MSZ_E_FORUM_TOPIC_DELETE_PERM;
|
||||
}
|
||||
|
||||
if($topic['author_user_id'] !== $userId) {
|
||||
return MSZ_E_FORUM_TOPIC_DELETE_OWNER;
|
||||
}
|
||||
|
||||
if(strtotime($topic['topic_created']) <= time() - MSZ_FORUM_TOPIC_DELETE_TIME_LIMIT) {
|
||||
return MSZ_E_FORUM_TOPIC_DELETE_OLD;
|
||||
}
|
||||
|
||||
$totalReplies = $topic['topic_count_posts'] + $topic['topic_count_posts_deleted'];
|
||||
|
||||
if($totalReplies > MSZ_E_FORUM_TOPIC_DELETE_POSTS) {
|
||||
return MSZ_E_FORUM_TOPIC_DELETE_POSTS;
|
||||
}
|
||||
}
|
||||
|
||||
return MSZ_E_FORUM_TOPIC_DELETE_OK;
|
||||
}
|
||||
|
||||
function forum_topic_delete(int $topicId): bool {
|
||||
if($topicId < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$markTopicDeleted = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_forum_topics`
|
||||
SET `topic_deleted` = NOW()
|
||||
WHERE `topic_id` = :topic
|
||||
AND `topic_deleted` IS NULL
|
||||
');
|
||||
$markTopicDeleted->bind('topic', $topicId);
|
||||
|
||||
if(!$markTopicDeleted->execute()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$markPostsDeleted = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_forum_posts` as p
|
||||
SET p.`post_deleted` = (
|
||||
SELECT `topic_deleted`
|
||||
FROM `msz_forum_topics`
|
||||
WHERE `topic_id` = p.`topic_id`
|
||||
)
|
||||
WHERE p.`topic_id` = :topic
|
||||
AND p.`post_deleted` IS NULL
|
||||
');
|
||||
$markPostsDeleted->bind('topic', $topicId);
|
||||
|
||||
return $markPostsDeleted->execute();
|
||||
}
|
||||
|
||||
function forum_topic_restore(int $topicId): bool {
|
||||
if($topicId < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$markPostsRestored = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_forum_posts` as p
|
||||
SET p.`post_deleted` = NULL
|
||||
WHERE p.`topic_id` = :topic
|
||||
AND p.`post_deleted` = (
|
||||
SELECT `topic_deleted`
|
||||
FROM `msz_forum_topics`
|
||||
WHERE `topic_id` = p.`topic_id`
|
||||
)
|
||||
');
|
||||
$markPostsRestored->bind('topic', $topicId);
|
||||
|
||||
if(!$markPostsRestored->execute()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$markTopicRestored = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_forum_topics`
|
||||
SET `topic_deleted` = NULL
|
||||
WHERE `topic_id` = :topic
|
||||
AND `topic_deleted` IS NOT NULL
|
||||
');
|
||||
$markTopicRestored->bind('topic', $topicId);
|
||||
|
||||
return $markTopicRestored->execute();
|
||||
}
|
||||
|
||||
function forum_topic_nuke(int $topicId): bool {
|
||||
if($topicId < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$nukeTopic = \Misuzu\DB::prepare('
|
||||
DELETE FROM `msz_forum_topics`
|
||||
WHERE `topic_id` = :topic
|
||||
');
|
||||
$nukeTopic->bind('topic', $topicId);
|
||||
return $nukeTopic->execute();
|
||||
}
|
||||
|
||||
function forum_topic_priority(int $topic): array {
|
||||
if($topic < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$getPriority = \Misuzu\DB::prepare('
|
||||
SELECT
|
||||
tp.`topic_id`, tp.`topic_priority`,
|
||||
u.`user_id`, u.`username`,
|
||||
COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour`
|
||||
FROM `msz_forum_topics_priority` AS tp
|
||||
LEFT JOIN `msz_users` AS u
|
||||
ON u.`user_id` = tp.`user_id`
|
||||
LEFT JOIN `msz_roles` AS r
|
||||
ON u.`display_role` = r.`role_id`
|
||||
WHERE `topic_id` = :topic
|
||||
');
|
||||
$getPriority->bind('topic', $topic);
|
||||
|
||||
return $getPriority->fetchAll();
|
||||
}
|
||||
|
||||
function forum_topic_priority_increase(int $topic, int $user, int $bump = 1): void {
|
||||
if($topic < 1 || $user < 1 || $bump === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bumpPriority = \Misuzu\DB::prepare('
|
||||
INSERT INTO `msz_forum_topics_priority`
|
||||
(`topic_id`, `user_id`, `topic_priority`)
|
||||
VALUES
|
||||
(:topic, :user, :bump1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`topic_priority` = `topic_priority` + :bump2
|
||||
');
|
||||
$bumpPriority->bind('topic', $topic);
|
||||
$bumpPriority->bind('user', $user);
|
||||
$bumpPriority->bind('bump1', $bump);
|
||||
$bumpPriority->bind('bump2', $bump);
|
||||
$bumpPriority->execute();
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<?php
|
||||
define('MSZ_TOPIC_TITLE_LENGTH_MIN', 3);
|
||||
define('MSZ_TOPIC_TITLE_LENGTH_MAX', 100);
|
||||
define('MSZ_POST_TEXT_LENGTH_MIN', 1);
|
||||
define('MSZ_POST_TEXT_LENGTH_MAX', 60000);
|
||||
|
||||
function forum_validate_title(string $title): string {
|
||||
$length = mb_strlen(trim($title));
|
||||
|
||||
if($length < MSZ_TOPIC_TITLE_LENGTH_MIN) {
|
||||
return 'too-short';
|
||||
}
|
||||
|
||||
if($length > MSZ_TOPIC_TITLE_LENGTH_MAX) {
|
||||
return 'too-long';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function forum_validate_post(string $text): string {
|
||||
$length = mb_strlen(trim($text));
|
||||
|
||||
if($length < MSZ_POST_TEXT_LENGTH_MIN) {
|
||||
return 'too-short';
|
||||
}
|
||||
|
||||
if($length > MSZ_POST_TEXT_LENGTH_MAX) {
|
||||
return 'too-long';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
91
src/Http/Handlers/Forum/ForumCategoryHandler.php
Normal file
91
src/Http/Handlers/Forum/ForumCategoryHandler.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
namespace Misuzu\Http\Handlers\Forum;
|
||||
|
||||
use HttpResponse;
|
||||
use HttpRequest;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Forum\ForumCategory;
|
||||
use Misuzu\Forum\ForumCategoryNotFoundException;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class ForumCategoryHandler extends ForumHandler {
|
||||
public function category(HttpResponse $response, HttpRequest $request, int $categoryId) {
|
||||
if($categoryId === 0) {
|
||||
$response->redirect(url('forum-index'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$categoryInfo = ForumCategory::byId($categoryId);
|
||||
} catch(ForumCategoryNotFoundException $ex) {}
|
||||
|
||||
if(empty($categoryInfo) || ($categoryInfo->isLink() && !$categoryInfo->hasLink()))
|
||||
return 404;
|
||||
|
||||
$currentUser = User::getCurrent();
|
||||
|
||||
if(!$categoryInfo->canView($currentUser))
|
||||
return 403;
|
||||
|
||||
$perms = forum_perms_get_user($categoryInfo->getId(), $currentUser === null ? 0 : $currentUser->getId())[MSZ_FORUM_PERMS_GENERAL];
|
||||
|
||||
if(isset($currentUser) && $currentUser->hasActiveWarning())
|
||||
$perms &= ~MSZ_FORUM_PERM_SET_WRITE;
|
||||
|
||||
if($categoryInfo->isLink()) {
|
||||
$categoryInfo->increaseLinkClicks();
|
||||
$response->redirect($categoryInfo->getLink());
|
||||
return;
|
||||
}
|
||||
|
||||
$canViewDeleted = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST);
|
||||
$pagination = new Pagination($categoryInfo->getActualTopicCount($canViewDeleted), 20);
|
||||
|
||||
if(!$pagination->hasValidOffset() && $pagination->getCount() > 0)
|
||||
return 404;
|
||||
|
||||
$response->setTemplate('forum.forum', [
|
||||
'forum_perms' => $perms,
|
||||
'forum_info' => $categoryInfo,
|
||||
'forum_pagination' => $pagination,
|
||||
'can_view_deleted' => $canViewDeleted,
|
||||
]);
|
||||
}
|
||||
|
||||
public function createView(HttpResponse $response, HttpRequest $request, int $categoryId) {
|
||||
try {
|
||||
$categoryInfo = ForumCategory::byId($categoryId);
|
||||
} catch(ForumCategoryNotFoundException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
var_dump($categoryInfo->getId());
|
||||
}
|
||||
|
||||
public function createAction(HttpResponse $response, HttpRequest $request, int $categoryId) {
|
||||
try {
|
||||
$categoryInfo = ForumCategory::byId($categoryId);
|
||||
} catch(ForumCategoryNotFoundException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
var_dump($categoryInfo->getId());
|
||||
}
|
||||
|
||||
public function legacy(HttpResponse $response, HttpRequest $request) {
|
||||
$categoryId = (int)$request->getQueryParam('f', FILTER_SANITIZE_NUMBER_INT);
|
||||
|
||||
if($categoryId < 0)
|
||||
return 404;
|
||||
|
||||
if($categoryId === 0) {
|
||||
$response->redirect(url('forum-index'));
|
||||
return;
|
||||
}
|
||||
|
||||
$response->redirect(url('forum-category', [
|
||||
'forum' => $categoryId,
|
||||
'page' => (int)$request->getQueryParam('p', FILTER_SANITIZE_NUMBER_INT),
|
||||
]));
|
||||
}
|
||||
}
|
6
src/Http/Handlers/Forum/ForumHandler.php
Normal file
6
src/Http/Handlers/Forum/ForumHandler.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace Misuzu\Http\Handlers\Forum;
|
||||
|
||||
use Misuzu\Http\Handlers\Handler;
|
||||
|
||||
abstract class ForumHandler extends Handler {}
|
56
src/Http/Handlers/Forum/ForumIndexHandler.php
Normal file
56
src/Http/Handlers/Forum/ForumIndexHandler.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
namespace Misuzu\Http\Handlers\Forum;
|
||||
|
||||
use HttpResponse;
|
||||
use HttpRequest;
|
||||
use Misuzu\Forum\ForumCategory;
|
||||
use Misuzu\Forum\ForumCategoryNotFoundException;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class ForumIndexHandler extends ForumHandler {
|
||||
public function index(HttpResponse $response): void {
|
||||
$response->setTemplate('forum.index', [
|
||||
'forum_root' => ForumCategory::root(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function markAsRead(HttpResponse $response, HttpRequest $request) {
|
||||
try {
|
||||
$categoryInfo = ForumCategory::byId(
|
||||
(int)($request->getBodyParam('forum', FILTER_SANITIZE_NUMBER_INT) ?? $request->getQueryParam('forum', FILTER_SANITIZE_NUMBER_INT))
|
||||
);
|
||||
} catch(ForumCategoryNotFoundException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
if($request->getMethod() === 'GET') {
|
||||
$response->setTemplate('confirm', [
|
||||
'title' => 'Mark forum as read',
|
||||
'message' => 'Are you sure you want to mark ' . ($categoryInfo->isRoot() ? 'the entire' : 'this') . ' forum as read?',
|
||||
'return' => url($categoryInfo->isRoot() ? 'forum-index' : 'forum-category', ['forum' => $categoryInfo->getId()]),
|
||||
'params' => [
|
||||
'forum' => $categoryInfo->getId(),
|
||||
]
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$categoryInfo->markAsRead(User::getCurrent());
|
||||
|
||||
$response->redirect(
|
||||
url($categoryInfo->isRoot() ? 'forum-index' : 'forum-category', ['forum' => $categoryInfo->getId()]),
|
||||
false,
|
||||
$request->hasHeader('X-Misuzu-XHR')
|
||||
);
|
||||
}
|
||||
|
||||
public function legacy(HttpResponse $response, HttpRequest $request): void {
|
||||
if($request->getQueryParam('m') === 'mark') {
|
||||
$forumId = (int)$request->getQueryParam('f', FILTER_SANITIZE_NUMBER_INT);
|
||||
$response->redirect(url($forumId < 1 ? 'forum-mark-global' : 'forum-mark-single', ['forum' => $forumId]));
|
||||
return;
|
||||
}
|
||||
|
||||
$response->redirect(url('forum-index'));
|
||||
}
|
||||
}
|
32
src/Http/Handlers/Forum/ForumPollHandler.php
Normal file
32
src/Http/Handlers/Forum/ForumPollHandler.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
namespace Misuzu\Http\Handlers\Forum;
|
||||
|
||||
use HttpResponse;
|
||||
use HttpRequest;
|
||||
use Misuzu\Forum\ForumPoll;
|
||||
use Misuzu\Forum\ForumPollNotFoundException;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class ForumPollHandler extends ForumHandler {
|
||||
public function vote(HttpResponse $response, HttpRequest $request, int $postId) {
|
||||
try {
|
||||
$pollInfo = ForumPoll::byId($pollId);
|
||||
} catch(ForumPollNotFoundException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
// check perms lol
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach($pollInfo->getOptions() as $optionInfo)
|
||||
$results[] = [
|
||||
'id' => $optionInfo->getId(),
|
||||
'text' => $optionInfo->getText(),
|
||||
'vote_count' => $optionInfo->getVotes(),
|
||||
'vote_percent' => $optionInfo->getPercentage(),
|
||||
];
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
43
src/Http/Handlers/Forum/ForumPostHandler.php
Normal file
43
src/Http/Handlers/Forum/ForumPostHandler.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
namespace Misuzu\Http\Handlers\Forum;
|
||||
|
||||
use HttpResponse;
|
||||
use HttpRequest;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Forum\ForumPost;
|
||||
use Misuzu\Forum\ForumPostNotFoundException;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class ForumPostHandler extends ForumHandler {
|
||||
public function post(HttpResponse $response, HttpRequest $request, int $postId) {
|
||||
try {
|
||||
$postInfo = ForumPost::byId($postId);
|
||||
} catch(ForumPostNotFoundException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
var_dump($postInfo->getId());
|
||||
}
|
||||
|
||||
public function edit(HttpResponse $response, HttpRequest $request, int $postId) {
|
||||
}
|
||||
|
||||
public function delete(HttpResponse $response, HttpRequest $request, int $postId) {
|
||||
}
|
||||
|
||||
public function restore(HttpResponse $response, HttpRequest $request, int $postId) {
|
||||
}
|
||||
|
||||
public function nuke(HttpResponse $response, HttpRequest $request, int $postId) {
|
||||
}
|
||||
|
||||
public function legacy(HttpResponse $response, HttpRequest $request) {
|
||||
$postId = (int)$request->getQueryParam('p', FILTER_SANITIZE_NUMBER_INT);
|
||||
if($postId > 0) {
|
||||
$response->redirect(url('forum-post', ['post' => $postId]));
|
||||
return;
|
||||
}
|
||||
|
||||
return 404;
|
||||
}
|
||||
}
|
132
src/Http/Handlers/Forum/ForumTopicHandler.php
Normal file
132
src/Http/Handlers/Forum/ForumTopicHandler.php
Normal file
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
namespace Misuzu\Http\Handlers\Forum;
|
||||
|
||||
use HttpResponse;
|
||||
use HttpRequest;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Forum\ForumTopic;
|
||||
use Misuzu\Forum\ForumTopicNotFoundException;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class ForumTopicHandler extends ForumHandler {
|
||||
public function topic(HttpResponse $response, HttpRequest $request, int $topicId) {
|
||||
try {
|
||||
$topicInfo = ForumTopic::byId($topicId);
|
||||
} catch(ForumTopicNotFoundException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
var_dump($topicInfo->getId());
|
||||
}
|
||||
|
||||
public function reply(HttpResponse $response, HttpRequest $request, int $topicId) {
|
||||
}
|
||||
|
||||
// Should support a since param to fetch a number of points after a point in time/post id
|
||||
public function live(HttpResponse $response, HttpRequest $request, int $topicId) {
|
||||
try {
|
||||
$topicInfo = ForumTopic::byId($topicId);
|
||||
} catch(ForumTopicNotFoundException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
if(!$topicInfo->getCategory()->canView(User::getCurrent()))
|
||||
return 403;
|
||||
|
||||
$sincePostId = (int)($request->getQueryParam('since', FILTER_SANITIZE_NUMBER_INT) ?? -1);
|
||||
|
||||
$ajaxInfo = [
|
||||
'id' => $topicInfo->getId(),
|
||||
'title' => $topicInfo->getTitle(),
|
||||
];
|
||||
|
||||
$categoryInfo = $topicInfo->getCategory();
|
||||
$ajaxInfo['category'] = [
|
||||
'id' => $categoryInfo->getId(),
|
||||
'name' => $categoryInfo->getName(),
|
||||
'tree' => [],
|
||||
];
|
||||
|
||||
$parentTree = $categoryInfo->getParentTree();
|
||||
foreach($parentTree as $parentInfo)
|
||||
$ajaxInfo['category']['tree'][] = [
|
||||
'id' => $parentInfo->getId(),
|
||||
'name' => $parentInfo->getName(),
|
||||
];
|
||||
|
||||
if($topicInfo->hasPriorityVoting()) {
|
||||
$ajaxInfo['priority'] = [
|
||||
'total' => $topicInfo->getPriority(),
|
||||
'votes' => [],
|
||||
];
|
||||
$topicPriority = $topicInfo->getPriorityVotes();
|
||||
foreach($topicPriority as $priorityInfo) {
|
||||
$priorityUserInfo = $priorityInfo->getUser();
|
||||
$ajaxInfo['priority']['votes'][] = [
|
||||
'count' => $priorityInfo->getPriority(),
|
||||
'user' => [
|
||||
'id' => $priorityUserInfo->getId(),
|
||||
'name' => $priorityUserInfo->getUsername(),
|
||||
'colour' => $priorityUserInfo->getColour()->getRaw(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if($topicInfo->hasPoll()) {
|
||||
$pollInfo = $topicInfo->getPoll();
|
||||
$ajaxInfo['poll'] = [
|
||||
'id' => $pollInfo->getId(),
|
||||
'options' => [],
|
||||
];
|
||||
|
||||
$pollOptions = $pollInfo->getOptions();
|
||||
foreach($pollOptions as $optionInfo)
|
||||
$ajaxInfo['poll']['options'][] = [
|
||||
'id' => $optionInfo->getId(),
|
||||
'text' => $optionInfo->getText(),
|
||||
'vote_count' => $optionInfo->getVotes(),
|
||||
'vote_percent' => $optionInfo->getPercentage(),
|
||||
];
|
||||
}
|
||||
|
||||
if($sincePostId >= 0) {
|
||||
// Should contain all info necessary to build said posts
|
||||
// Maybe just serialised HTML a la YTKNS?
|
||||
$ajaxInfo['posts'] = [];
|
||||
}
|
||||
|
||||
return $ajaxInfo;
|
||||
}
|
||||
|
||||
public function delete(HttpResponse $response, HttpRequest $request, int $topicId) {
|
||||
}
|
||||
|
||||
public function restore(HttpResponse $response, HttpRequest $request, int $topicId) {
|
||||
}
|
||||
|
||||
public function nuke(HttpResponse $response, HttpRequest $request, int $topicId) {
|
||||
}
|
||||
|
||||
public function bump(HttpResponse $response, HttpRequest $request, int $topicId) {
|
||||
}
|
||||
|
||||
public function lock(HttpResponse $response, HttpRequest $request, int $topicId) {
|
||||
}
|
||||
|
||||
public function legacy(HttpResponse $response, HttpRequest $request) {
|
||||
$postId = (int)$request->getQueryParam('p', FILTER_SANITIZE_NUMBER_INT);
|
||||
if($postId > 0) {
|
||||
$response->redirect(url('forum-post', ['post' => $postId]));
|
||||
return;
|
||||
}
|
||||
|
||||
$topicId = (int)$request->getQueryParam('t', FILTER_SANITIZE_NUMBER_INT);
|
||||
if($topicId > 0) {
|
||||
$response->redirect(url('forum-topic', ['topic' => $topicId]));
|
||||
return;
|
||||
}
|
||||
|
||||
return 404;
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
<?php
|
||||
namespace Misuzu\Http\Handlers;
|
||||
|
||||
use HttpResponse;
|
||||
use HttpRequest;
|
||||
use Misuzu\CSRF;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
final class ForumHandler extends Handler {
|
||||
public function markAsReadGET(HttpResponse $response, HttpRequest $request): void {
|
||||
$forumId = (int)$request->getQueryParam('forum', FILTER_SANITIZE_NUMBER_INT);
|
||||
$response->setTemplate('confirm', [
|
||||
'title' => 'Mark forum as read',
|
||||
'message' => 'Are you sure you want to mark ' . ($forumId === null ? 'the entire' : 'this') . ' forum as read?',
|
||||
'return' => url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]),
|
||||
'params' => [
|
||||
'forum' => $forumId,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function markAsReadPOST(HttpResponse $response, HttpRequest $request) {
|
||||
$forumId = (int)$request->getBodyParam('forum', FILTER_SANITIZE_NUMBER_INT);
|
||||
forum_mark_read($forumId, User::getCurrent()->getId());
|
||||
|
||||
$response->redirect(
|
||||
url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]),
|
||||
false,
|
||||
$request->hasHeader('X-Misuzu-XHR')
|
||||
);
|
||||
}
|
||||
}
|
|
@ -19,7 +19,8 @@ class Route implements Serializable {
|
|||
public function __construct(array $methods, string $path, ?string $method = null, ?string $class = null) {
|
||||
$this->methods = array_map('strtoupper', $methods);
|
||||
$this->path = $path;
|
||||
$this->handlerClass = $class;
|
||||
if($class !== null)
|
||||
$this->handlerClass = str_replace('.', '\\', $class);
|
||||
$this->handlerMethod = $method;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,13 +15,20 @@ class Memoizer {
|
|||
|
||||
if(is_callable($find)) {
|
||||
$item = array_find($this->collection, $find) ?? $create();
|
||||
if(method_exists($item, 'getId'))
|
||||
$this->collection[$item->getId()] = $item;
|
||||
else
|
||||
$this->collection[] = $item;
|
||||
if($item !== null)
|
||||
$this->insert($item);
|
||||
return $item;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Wasn\'t able to figure out your $find argument.');
|
||||
}
|
||||
|
||||
public function insert($item): void {
|
||||
if($item === null)
|
||||
throw new InvalidArgumentException('null');
|
||||
if(method_exists($item, 'getId'))
|
||||
$this->collection[$item->getId()] = $item;
|
||||
else
|
||||
$this->collection[] = $item;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,4 +74,60 @@ final class Pagination {
|
|||
|
||||
return $default;
|
||||
}
|
||||
|
||||
private const PAGE_RANGE = 5;
|
||||
|
||||
public function render(string $pathOrName, array $params = [], string $pageParam = self::DEFAULT_PARAM, string $urlFragment = ''): string {
|
||||
if($this->getPages() <= 1)
|
||||
return '';
|
||||
|
||||
if($pathOrName[0] !== '/')
|
||||
$pathOrName = url($pathOrName);
|
||||
|
||||
$getUrl = function(int $page) use ($pathOrName, $params, $pageParam, $urlFragment) {
|
||||
if($page <= 1)
|
||||
unset($params[$pageParam]);
|
||||
else
|
||||
$params[$pageParam] = $page;
|
||||
|
||||
$url = $pathOrName;
|
||||
if(!empty($params))
|
||||
$url .= '?' . http_build_query($params);
|
||||
if(!empty($urlFragment))
|
||||
$url .= '#' . rawurldecode($urlFragment);
|
||||
|
||||
return $url;
|
||||
};
|
||||
|
||||
$html = '<div class="pagination">';
|
||||
|
||||
$html .= '<div class="pagination__section pagination__section--first">';
|
||||
if($this->getPage() <= 1) {
|
||||
$html .= '<div class="pagination__link pagination__link--first pagination__link--disabled"><i class="fas fa-angle-double-left"></i></div>';
|
||||
$html .= '<div class="pagination__link pagination__link--prev pagination__link--disabled"><i class="fas fa-angle-left"></i></div>';
|
||||
} else {
|
||||
$html .= '<a href="' . $getUrl(1) . '" class="pagination__link pagination__link--first" rel="first"><i class="fas fa-angle-double-left"></i></a>';
|
||||
$html .= '<a href="' . $getUrl($this->getPage() - 1) . '" class="pagination__link pagination__link--prev" rel="prev"><i class="fas fa-angle-left"></i></a>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="pagination__section pagination__section--pages">';
|
||||
$start = max($this->getPage() - self::PAGE_RANGE, 1);
|
||||
$stop = min($this->getPage() + self::PAGE_RANGE, $this->getPages());
|
||||
for($i = $start; $i <= $stop; ++$i)
|
||||
$html .= '<a href="' . $getUrl($i) . '" class="pagination__link' . ($i === $this->getPage() ? ' pagination__link--current' : '') . '">' . number_format($i) . '</a>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="pagination__section pagination__section--last">';
|
||||
if($this->getPage() >= $this->getPages()) {
|
||||
$html .= '<div class="pagination__link pagination__link--next pagination__link--disabled"><i class="fas fa-angle-right"></i></div>';
|
||||
$html .= '<div class="pagination__link pagination__link--last pagination__link--disabled"><i class="fas fa-angle-double-right"></i></div>';
|
||||
} else {
|
||||
$html .= '<a href="' . $getUrl($this->getPage() + 1) . '" class="pagination__link pagination__link--next" rel="next"><i class="fas fa-angle-right"></i></a>';
|
||||
$html .= '<a href="' . $getUrl($this->getPages()) . '" class="pagination__link pagination__link--last" rel="last"><i class="fas fa-angle-double-right"></i></a>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
return $html . '</div>';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,10 +23,6 @@ final class TwigMisuzu extends Twig_Extension {
|
|||
new Twig_Function('url_construct', 'url_construct'),
|
||||
new Twig_Function('url', 'url'),
|
||||
new Twig_Function('url_list', 'url_list'),
|
||||
new Twig_Function('html_avatar', 'html_avatar'),
|
||||
new Twig_Function('forum_may_have_children', 'forum_may_have_children'),
|
||||
new Twig_Function('forum_may_have_topics', 'forum_may_have_topics'),
|
||||
new Twig_Function('forum_has_priority_voting', 'forum_has_priority_voting'),
|
||||
new Twig_Function('csrf_token', fn() => CSRF::token()),
|
||||
new Twig_Function('git_commit_hash', fn(bool $long = false) => GitInfo::hash($long)),
|
||||
new Twig_Function('git_tag', fn() => GitInfo::tag()),
|
||||
|
|
|
@ -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;
|
||||
|
@ -303,6 +304,11 @@ class User implements HasRankInterface, JsonSerializable {
|
|||
return Parser::instance($this->getForumSignatureParser())
|
||||
->parseText(htmlspecialchars($this->getForumSignatureText()));
|
||||
}
|
||||
public function getForumSignatureClasses(): string {
|
||||
if($this->getForumSignatureParser() === Parser::MARKDOWN)
|
||||
return 'markdown';
|
||||
return '';
|
||||
}
|
||||
|
||||
// Address these through getBackgroundInfo()
|
||||
public function getBackgroundSettings(): int {
|
||||
|
@ -343,6 +349,17 @@ class User implements HasRankInterface, JsonSerializable {
|
|||
return intval($this->getBirthdate()->diff(new DateTime('now', new DateTimeZone('UTC')))->format('%y'));
|
||||
}
|
||||
|
||||
private $preferredParser = null;
|
||||
public function getPreferredParser(): int {
|
||||
if($this->preferredParser === null)
|
||||
$this->preferredParser = DB::prepare(
|
||||
'SELECT `post_parse` FROM `msz_forum_posts`'
|
||||
. ' WHERE `user_id` = :user AND `post_deleted` IS NULL'
|
||||
. ' ORDER BY `post_id` DESC LIMIT 1'
|
||||
)->bind('user', $this->getId())->fetchColumn() ?? Parser::BBCODE;
|
||||
return $this->preferredParser;
|
||||
}
|
||||
|
||||
public function profileFields(bool $filterEmpty = true): array {
|
||||
if(($userId = $this->getId()) < 1)
|
||||
return [];
|
||||
|
@ -883,7 +900,7 @@ class User implements HasRankInterface, JsonSerializable {
|
|||
return $user;
|
||||
});
|
||||
}
|
||||
public static function byUsername(string $username): ?self {
|
||||
public static function byUsername(string $username): self {
|
||||
$username = mb_strtolower($username);
|
||||
return self::memoizer()->find(function($user) use ($username) {
|
||||
return mb_strtolower($user->getUsername()) === $username;
|
||||
|
@ -896,7 +913,7 @@ class User implements HasRankInterface, JsonSerializable {
|
|||
return $user;
|
||||
});
|
||||
}
|
||||
public static function byEMailAddress(string $address): ?self {
|
||||
public static function byEMailAddress(string $address): self {
|
||||
$address = mb_strtolower($address);
|
||||
return self::memoizer()->find(function($user) use ($address) {
|
||||
return mb_strtolower($user->getEmailAddress()) === $address;
|
||||
|
@ -928,7 +945,7 @@ class User implements HasRankInterface, JsonSerializable {
|
|||
return DB::prepare(self::byQueryBase() . ' WHERE `user_deleted` IS NULL ORDER BY `user_id` DESC LIMIT 1')
|
||||
->fetchObject(self::class);
|
||||
}
|
||||
public static function findForProfile($userIdOrName): ?self {
|
||||
public static function findForProfile($userIdOrName): self {
|
||||
$userIdOrNameLower = mb_strtolower($userIdOrName);
|
||||
return self::memoizer()->find(function($user) use ($userIdOrNameLower) {
|
||||
return $user->getId() == $userIdOrNameLower || mb_strtolower($user->getUsername()) === $userIdOrNameLower;
|
||||
|
|
|
@ -45,7 +45,7 @@ define('MSZ_URLS', [
|
|||
'forum-mark-single' => ['/forum/mark-as-read', ['forum' => '<forum>']],
|
||||
'forum-topic-new' => ['/forum/posting.php', ['f' => '<forum>']],
|
||||
'forum-reply-new' => ['/forum/posting.php', ['t' => '<topic>']],
|
||||
'forum-category' => ['/forum/forum.php', ['f' => '<forum>', 'p' => '<page>']],
|
||||
'forum-category' => ['/forum/<forum>', ['p' => '<page>']],
|
||||
'forum-topic' => ['/forum/topic.php', ['t' => '<topic>', 'page' => '<page>']],
|
||||
'forum-topic-create' => ['/forum/posting.php', ['f' => '<forum>']],
|
||||
'forum-topic-bump' => ['/forum/topic.php', ['t' => '<topic>', 'm' => 'bump', 'csrf' => '{csrf}']],
|
||||
|
@ -55,6 +55,7 @@ define('MSZ_URLS', [
|
|||
'forum-topic-restore' => ['/forum/topic.php', ['t' => '<topic>', 'm' => 'restore', 'csrf' => '{csrf}']],
|
||||
'forum-topic-nuke' => ['/forum/topic.php', ['t' => '<topic>', 'm' => 'nuke', 'csrf' => '{csrf}']],
|
||||
'forum-topic-priority' => ['/forum/topic-priority.php', ['t' => '<topic>', 'b' => '<bump>']],
|
||||
'forum-topic-live' => ['/forum/topic/<topic>/live', ['since' => '<post>']],
|
||||
'forum-post' => ['/forum/topic.php', ['p' => '<post>'], '<post_fragment>'],
|
||||
'forum-post-create' => ['/forum/posting.php', ['t' => '<topic>']],
|
||||
'forum-post-delete' => ['/forum/post.php', ['p' => '<post>', 'm' => 'delete']],
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'changelog/master.twig' %}
|
||||
{% from 'macros.twig' import pagination, container_title %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
{% from 'changelog/macros.twig' import changelog_listing %}
|
||||
{% from '_layout/comments.twig' import comments_section %}
|
||||
|
||||
|
@ -39,7 +39,7 @@
|
|||
|
||||
{% if not is_date %}
|
||||
<div class="changelog__pagination">
|
||||
{{ pagination(changelog_pagination, url('changelog-index'), null, {'date':changelog_date_fmt, 'user':changelog_user.id|default(0)})}}
|
||||
{{ changelog_pagination.render('changelog-index', {'date':changelog_date_fmt, 'user':changelog_user.id|default(0)})|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -1,32 +1,30 @@
|
|||
{% extends 'forum/master.twig' %}
|
||||
{% from 'forum/macros.twig' import forum_category_listing, forum_topic_listing, forum_category_buttons, forum_header, forum_category_tools %}
|
||||
|
||||
{% set title = forum_info.forum_name %}
|
||||
{% set title = forum_info.name %}
|
||||
{% set canonical_url = url('forum-category', {
|
||||
'forum': forum_info.forum_id,
|
||||
'forum': forum_info.id,
|
||||
'page': forum_pagination.page|default(0) > 1 ? forum_pagination.page : 0,
|
||||
}) %}
|
||||
|
||||
{% block content %}
|
||||
{{ forum_header(forum_info.forum_name, forum_breadcrumbs, true, canonical_url, [
|
||||
{{ forum_header(forum_info, true, canonical_url, [
|
||||
{
|
||||
'html': '<i class="far fa-check-circle"></i> Mark as Read',
|
||||
'url': url('forum-mark-single', {'forum': forum_info.forum_id}),
|
||||
'url': url('forum-mark-single', {'forum': forum_info.id}),
|
||||
'display': current_user is defined,
|
||||
'method': 'POST',
|
||||
}
|
||||
]) }}
|
||||
|
||||
{% if forum_may_have_children and forum_info.forum_subforums|length > 0 %}
|
||||
{{ forum_category_listing(forum_info.forum_subforums, 'Forums') }}
|
||||
{% endif %}
|
||||
{{ forum_category_listing(forum_info, current_user, 'Forums') }}
|
||||
|
||||
{% if forum_may_have_topics %}
|
||||
{% if forum_info.canHaveTopics %}
|
||||
{% set category_tools = forum_category_tools(forum_info, forum_perms, forum_pagination) %}
|
||||
{{ category_tools }}
|
||||
{{ forum_topic_listing(forum_topics) }}
|
||||
{{ forum_topic_listing(forum_info.topics(can_view_deleted, forum_pagination), null, current_user) }}
|
||||
{{ category_tools }}
|
||||
{% endif %}
|
||||
|
||||
{{ forum_header('', forum_breadcrumbs) }}
|
||||
{{ forum_header(forum_info, false) }}
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,21 +3,21 @@
|
|||
{% from 'forum/macros.twig' import forum_category_listing %}
|
||||
|
||||
{% set title = 'Forum Listing' %}
|
||||
{% set canonical_url = '/forum/' %}
|
||||
{% set canonical_url = url('forum-index') %}
|
||||
|
||||
{% block content %}
|
||||
{% if not forum_empty %}
|
||||
{% for category in forum_categories %}
|
||||
{% if category.forum_children > 0 %}
|
||||
{{ forum_category_listing(
|
||||
category.forum_subforums,
|
||||
category.forum_name,
|
||||
category.forum_colour,
|
||||
category.forum_id == constant('MSZ_FORUM_ROOT')
|
||||
? ''
|
||||
: 'f' ~ category.forum_id,
|
||||
category.forum_icon|default('')
|
||||
) }}
|
||||
{% if forum_root.children|length is empty %}
|
||||
<div class="container">
|
||||
{{ container_title('<i class="fas fa-comment-slash fa-fw"></i> Forums') }}
|
||||
|
||||
<div class="container__content">
|
||||
<p>There are currently no visible forums.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% for category in forum_root.children %}
|
||||
{% if category.children is not empty %}
|
||||
{{ forum_category_listing(category, current_user|default(null)) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
|
@ -26,13 +26,5 @@
|
|||
<a href="{{ url('forum-mark-global') }}" class="input__button forum__actions__button" data-msz-method="POST">Mark All Read</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="container">
|
||||
{{ container_title('<i class="fas fa-comment-slash fa-fw"></i> Forums') }}
|
||||
|
||||
<div class="container__content">
|
||||
<p>There are currently no visible forums.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% extends 'forum/master.twig' %}
|
||||
{% from 'macros.twig' import avatar %}
|
||||
{% from 'forum/macros.twig' import forum_header %}
|
||||
|
||||
{% set title = 'Forum Leaderboard » ' ~ leaderboard_name %}
|
||||
{% set canonical_url = url('forum-leaderboard', {
|
||||
|
@ -9,18 +8,23 @@
|
|||
}) %}
|
||||
|
||||
{% block content %}
|
||||
{{ forum_header(title, [], false, canonical_url, [
|
||||
{
|
||||
'html': '<i class="fab fa-markdown fa-fw"></i> Markdown',
|
||||
'url': url('forum-leaderboard', {'id': leaderboard_id, 'mode': 'markdown'}),
|
||||
'display': leaderboard_mode != 'markdown',
|
||||
},
|
||||
{
|
||||
'html': '<i class="fas fa-table fa-fw"></i> Table',
|
||||
'url': url('forum-leaderboard', {'id': leaderboard_id}),
|
||||
'display': leaderboard_mode == 'markdown',
|
||||
},
|
||||
]) }}
|
||||
<div class="container forum__header">
|
||||
<a class="forum__header__title" href="{{ canonical_url }}">
|
||||
{{ title }}
|
||||
</a>
|
||||
|
||||
<div class="forum__header__actions">
|
||||
{% if leaderboard_mode == 'markdown' %}
|
||||
<a class="forum__header__action" href="{{ url('forum-leaderboard', {'id': leaderboard_id}) }}">
|
||||
<i class="fas fa-table fa-fw"></i> Table
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="forum__header__action" href="{{ url('forum-leaderboard', {'id': leaderboard_id, 'mode': 'markdown'}) }}">
|
||||
<i class="fas fa-markdown fa-fw"></i> Markdown
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container forum__leaderboard__categories">
|
||||
{% for id, name in leaderboard_categories %}
|
||||
|
|
|
@ -1,4 +1,44 @@
|
|||
{% macro forum_category_listing(forums, title, colour, id, icon) %}
|
||||
{% if forums is iterable %}
|
||||
{% from _self import forum_category_listing_old %}
|
||||
{{ forum_category_listing_old(forums, title, colour, id, icon) }}
|
||||
{% else %}
|
||||
{% from _self import forum_category_listing_new %}
|
||||
{{ forum_category_listing_new(forums, title, colour) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_category_listing_new(category, user, title) %}
|
||||
{% from _self import forum_category_entry %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
|
||||
{% if category.canView(user) %}
|
||||
{% set children = category.children(user) %}
|
||||
|
||||
{% if children is not empty or category.isCategoryForum %}
|
||||
{% set icon = title is not empty ? 'fas fa-folder fa-fw' : category.icon %}
|
||||
<div class="container forum__categories"
|
||||
{% if not category.colour.inherit %}style="--accent-colour: {{ category.colour }}"{% endif %}
|
||||
{% if not category.isRoot %}id="{{ category.id }}"{% endif %}>
|
||||
{{ container_title('<span class="' ~ icon ~ '"></span> ' ~ title|default(category.name)) }}
|
||||
|
||||
{% if children is empty %}
|
||||
<div class="forum__categories__empty">
|
||||
This category is empty.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="forum__categories__list">
|
||||
{% for category in children %}
|
||||
{{ forum_category_entry(category, user) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_category_listing_old(forums, title, colour, id, icon) %}
|
||||
{% from _self import forum_category_entry %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
|
||||
|
@ -21,32 +61,29 @@
|
|||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_header(title, breadcrumbs, omit_last_breadcrumb, title_url, actions) %}
|
||||
{% macro forum_header(info, is_top, title_url, actions, is_topic, title) %}
|
||||
<div class="container forum__header">
|
||||
{% if breadcrumbs is iterable and breadcrumbs|length > 0 %}
|
||||
<div class="forum__header__breadcrumbs">
|
||||
{% for name, url in breadcrumbs %}
|
||||
{% if url != breadcrumbs|first %}
|
||||
<div class="forum__header__breadcrumb__separator">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% set parents = info.parentTree %}
|
||||
<div class="forum__header__breadcrumbs">
|
||||
{% for parent in parents %}
|
||||
<a href="{{ parent.url }}" class="forum__header__breadcrumb">{{ parent.name }}</a>
|
||||
<div class="forum__header__breadcrumb__separator">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not is_top or is_topic %}
|
||||
<a href="{{ info.url }}" class="forum__header__breadcrumb">{{ info.name }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not (omit_last_breadcrumb|default(false) and url == breadcrumbs|last) %}
|
||||
<a href="{{ url }}" class="forum__header__breadcrumb">{{ name }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if title|length > 0 %}
|
||||
{% if is_top %}
|
||||
{% if title_url|length > 0 %}
|
||||
<a class="forum__header__title" href="{{ title_url }}">
|
||||
{{ title }}
|
||||
{{ title|default(info.name) }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="forum__header__title forum__header__title--fill">
|
||||
{{ title }}
|
||||
{{ title|default(info.name) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -65,49 +102,145 @@
|
|||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_category_tools(info, perms, pagination_info) %}
|
||||
{% from 'macros.twig' import pagination %}
|
||||
|
||||
{% set is_locked = info.forum_archived != 0 %}
|
||||
{% set can_topic = not is_locked and perms|perms_check(constant('MSZ_FORUM_PERM_CREATE_TOPIC')) %}
|
||||
{% set pag = pagination(pagination_info, url('forum-category'), null, {'f': info.forum_id}) %}
|
||||
{% macro forum_category_tools(category, perms, pagination_info) %}
|
||||
{% set can_topic = not category.isArchived and perms|perms_check(constant('MSZ_FORUM_PERM_CREATE_TOPIC')) %}
|
||||
{% set pag = pagination_info.render(url('forum-category', {'forum': category.id})) %}
|
||||
|
||||
{% if can_topic or pag|trim|length > 0 %}
|
||||
<div class="container forum__actions">
|
||||
<div class="forum__actions__buttons">
|
||||
{% if can_topic %}
|
||||
<a href="{{ url('forum-topic-new', {'forum': info.forum_id}) }}" class="input__button forum__actions__button">{{ info.forum_type == constant('MSZ_FORUM_TYPE_FEATURE') ? 'New Request' : 'New Topic' }}</a>
|
||||
<a href="{{ url('forum-topic-new', {'forum': category.id}) }}" class="input__button forum__actions__button">{{ category.canHavePriorityVotes ? 'New Request' : 'New Topic' }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="forum__actions__pagination">
|
||||
{{ pag }}
|
||||
{{ pag|raw }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_topic_tools(info, pagination_info, can_reply) %}
|
||||
{% from 'macros.twig' import pagination %}
|
||||
{% set pag = pagination_info.render('forum-topic', {'t': info.topic_id|default(info.id)}, 'page') %}
|
||||
|
||||
{% set pag = pagination(pagination_info, url('forum-topic'), null, {'t': info.topic_id}, 'page') %}
|
||||
|
||||
{% if can_reply or pag|trim|length > 0 %}
|
||||
{% if can_reply or pag|length > 0 %}
|
||||
<div class="container forum__actions">
|
||||
<div class="forum__actions__buttons">
|
||||
{% if can_reply %}
|
||||
<a href="{{ url('forum-reply-new', {'topic': info.topic_id}) }}" class="input__button">Reply</a>
|
||||
<a href="{{ url('forum-reply-new', {'topic': info.topic_id|default(info.id)}) }}" class="input__button">Reply</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="forum__actions__pagination">
|
||||
{{ pag }}
|
||||
{{ pag|raw }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_category_entry(forum, forum_unread, forum_icon) %}
|
||||
{% if forum.forum_id is defined %}
|
||||
{% from _self import forum_category_entry_old %}
|
||||
{{ forum_category_entry_old(forum, forum_unread, forum_icon) }}
|
||||
{% else %}
|
||||
{% from _self import forum_category_entry_new %}
|
||||
{{ forum_category_entry_new(forum, forum_unread) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_category_entry_new(category, user) %}
|
||||
{% from 'macros.twig' import avatar %}
|
||||
|
||||
<div class="forum__category">
|
||||
<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--{{ user is null or category.hasRead(user) ? '' : 'un' }}read">
|
||||
<span class="{{ category.icon }}"></span>
|
||||
</div>
|
||||
|
||||
<div class="forum__category__details">
|
||||
<div class="forum__category__title">
|
||||
{{ category.name }}
|
||||
</div>
|
||||
|
||||
{% if category.hasDescription %}
|
||||
<div class="forum__category__description">
|
||||
{{ category.parsedDescription|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if category.children is not empty %}
|
||||
<div class="forum__category__subforums">
|
||||
{% for child in category.children %}
|
||||
{% if child.canView(user) %}
|
||||
<a href="{{ url('forum-category', {'forum': child.id}) }}"
|
||||
class="forum__category__subforum{% if user is not null and not child.hasRead(user) %} forum__category__subforum--unread{% endif %}">
|
||||
{{ child.name }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if category.isLink %}
|
||||
{% if category.shouldCountLinkClicks %}
|
||||
<div class="forum__category__stats">
|
||||
<div class="forum__category__stat" title="Clicks">{{ category.linkClicks|number_format }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif category.canHaveChildren %}
|
||||
<div class="forum__category__stats">
|
||||
<div class="forum__category__stat" title="Topics">{{ category.topicCount|number_format }}</div>
|
||||
<div class="forum__category__stat" title="Posts">{{ category.postCount|number_format }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if category.canHaveTopics or category.shouldCountLinkClicks %}
|
||||
<div class="forum__category__activity{% if category.shouldCountLinkClicks %} forum__category__activity--empty{% endif %}">
|
||||
{% if not category.isLink %}
|
||||
{% set topic = category.latestTopic(user) %}
|
||||
{% set post = topic.lastPost|default(null) %}
|
||||
{% if topic is empty or topic.lastPost is empty %}
|
||||
<div class="forum__category__activity__none">
|
||||
There are no posts in this forum yet.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="forum__category__activity__details">
|
||||
<a class="forum__category__activity__post"
|
||||
href="{{ url('forum-post', {'post': post.id, 'post_fragment': 'p' ~ post.id}) }}">
|
||||
{{ topic.title }}
|
||||
</a>
|
||||
|
||||
<div class="forum__category__activity__info">
|
||||
<time datetime="{{ post.createdTime|date('c') }}"
|
||||
title="{{ post.createdTime|date('r') }}">{{ post.createdTime|time_diff }}</time>
|
||||
{% if post.hasUser %}
|
||||
by
|
||||
<a href="{{ url('user-profile', {'user': post.user.id}) }}" class="forum__category__username"
|
||||
style="--user-colour: {{ post.user.colour }}">
|
||||
{{ post.user.username }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if post.hasUser %}
|
||||
<a href="{{ url('user-profile', {'user': post.user.id}) }}" class="avatar forum__category__avatar">
|
||||
{{ avatar(post.user.id, 40, post.user.username) }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_category_entry_old(forum, forum_unread, forum_icon) %}
|
||||
{% from 'macros.twig' import avatar %}
|
||||
{% set forum_unread = forum_unread|default(forum.forum_unread|default(false)) ? 'unread' : 'read' %}
|
||||
|
||||
|
@ -116,12 +249,12 @@
|
|||
{% set forum_icon = forum.forum_icon %}
|
||||
{% elseif forum.forum_archived is defined and forum.forum_archived %}
|
||||
{% set forum_icon = 'fas fa-archive fa-fw' %}
|
||||
{% elseif forum.forum_type is defined and forum.forum_type != constant('MSZ_FORUM_TYPE_DISCUSSION') %}
|
||||
{% if forum.forum_type == constant('MSZ_FORUM_TYPE_FEATURE') %}
|
||||
{% elseif forum.forum_type is defined and forum.forum_type != constant('\\Misuzu\\Forum\\ForumCategory::TYPE_DISCUSSION') %}
|
||||
{% if forum.forum_type == constant('\\Misuzu\\Forum\\ForumCategory::TYPE_FEATURE') %}
|
||||
{% set forum_icon = 'fas fa-star fa-fw' %}
|
||||
{% elseif forum.forum_type == constant('MSZ_FORUM_TYPE_LINK') %}
|
||||
{% elseif forum.forum_type == constant('\\Misuzu\\Forum\\ForumCategory::TYPE_LINK') %}
|
||||
{% set forum_icon = 'fas fa-link fa-fw' %}
|
||||
{% elseif forum.forum_type == constant('MSZ_FORUM_TYPE_CATEGORY') %}
|
||||
{% elseif forum.forum_type == constant('\\Misuzu\\Forum\\ForumCategory::TYPE_CATEGORY') %}
|
||||
{% set forum_icon = 'fas fa-folder fa-fw' %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
|
@ -158,22 +291,22 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if forum.forum_type == constant('MSZ_FORUM_TYPE_LINK') %}
|
||||
{% if forum.forum_type == constant('\\Misuzu\\Forum\\ForumCategory::TYPE_LINK') %}
|
||||
{% if forum.forum_link_clicks is not null %}
|
||||
<div class="forum__category__stats">
|
||||
<div class="forum__category__stat" title="Clicks">{{ forum.forum_link_clicks|number_format }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif forum_may_have_children(forum.forum_type) %}
|
||||
{% elseif forum.forum_type in constant('\\Misuzu\\Forum\\ForumCategory::HAS_CHILDREN') %}
|
||||
<div class="forum__category__stats">
|
||||
<div class="forum__category__stat" title="Topics">{{ forum.forum_count_topics|number_format }}</div>
|
||||
<div class="forum__category__stat" title="Posts">{{ forum.forum_count_posts|number_format }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if forum_may_have_topics(forum.forum_type) or forum.forum_link_clicks is not null %}
|
||||
{% if forum.forum_type in constant('\\Misuzu\\Forum\\ForumCategory::HAS_TOPICS') or forum.forum_link_clicks is not null %}
|
||||
<div class="forum__category__activity{% if forum.forum_link_clicks is not null %} forum__category__activity--empty{% endif %}">
|
||||
{% if forum.forum_type != constant('MSZ_FORUM_TYPE_LINK') %}
|
||||
{% if forum.forum_type != constant('\\Misuzu\\Forum\\ForumCategory::TYPE_LINK') %}
|
||||
{% if forum.recent_topic_id is not defined %}
|
||||
<div class="forum__category__activity__none">
|
||||
There are no posts in this forum yet.
|
||||
|
@ -212,7 +345,7 @@
|
|||
{% endmacro %}
|
||||
|
||||
{% macro forum_topic_locked(locked, archived) %}
|
||||
{% if locked is not null or archived %}
|
||||
{% if locked|default(0) > 0 is not null or archived %}
|
||||
<div class="container forum__status">
|
||||
<div class="forum__status__icon">
|
||||
<div class="forum__status__icon__background"></div>
|
||||
|
@ -223,16 +356,14 @@
|
|||
This topic has been <span class="forum__status__emphasis">archived</span>.
|
||||
{% else %}
|
||||
This topic was locked
|
||||
<time class="forum__status__emphasis"
|
||||
datetime="{{ locked|date('c') }}"
|
||||
title="{{ locked|date('r') }}">{{ locked|time_diff }}</time>.
|
||||
<time class="forum__status__emphasis" datetime="{{ locked|date('c') }}" title="{{ locked|date('r') }}">{{ locked|time_diff }}</time>.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_topic_listing(topics, title) %}
|
||||
{% macro forum_topic_listing(topics, title, user) %}
|
||||
{% from _self import forum_topic_entry %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
|
||||
|
@ -242,7 +373,7 @@
|
|||
<div class="forum__topics__list">
|
||||
{% if topics|length > 0 %}
|
||||
{% for topic in topics %}
|
||||
{{ forum_topic_entry(topic) }}
|
||||
{{ forum_topic_entry(topic, user) }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="forum__topics__empty">
|
||||
|
@ -254,18 +385,122 @@
|
|||
{% endmacro %}
|
||||
|
||||
{% macro forum_topic_entry(topic, topic_icon, topic_unread) %}
|
||||
{% if topic.getId is defined %}
|
||||
{% from _self import forum_topic_entry_new %}
|
||||
{{ forum_topic_entry_new(topic, topic_icon) }}
|
||||
{% else %}
|
||||
{% from _self import forum_topic_entry_old %}
|
||||
{{ forum_topic_entry_old(topic, topic_icon, topic_unread) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_topic_entry_new(topic, user) %}
|
||||
{% from 'macros.twig' import avatar %}
|
||||
|
||||
<div class="forum__topic{% if topic.deleted %} forum__topic--deleted{% elseif topic.locked and not topic.important %} forum__topic--locked{% endif %}">
|
||||
<a href="{{ url('forum-topic', {'topic': topic.id}) }}" class="forum__topic__link"></a>
|
||||
|
||||
<div class="forum__topic__container">
|
||||
<div class="forum__topic__icon forum__topic__icon--{{ 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 %}
|
||||
<div class="forum__topic__icon__priority">{{ topic.priority|number_format }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if topic.hasParticipated(user) %}
|
||||
<div class="forum__topic__icon__participated" title="You have posted in this topic"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="forum__topic__details">
|
||||
<div class="forum__topic__title">
|
||||
<span class="forum__topic__title__inner">
|
||||
{{ topic.title }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="forum__topic__info">
|
||||
{% if topic.user is not empty %}
|
||||
by <a href="{{ url('user-profile', {'user': topic.user.id}) }}"
|
||||
class="forum__topic__username"
|
||||
style="--user-colour: {{ topic.user.colour }}">{{ topic.user.username }}</a>,
|
||||
|
||||
{% endif %}
|
||||
<time datetime="{{ topic.createdTime|date('c') }}" title="{{ topic.createdTime|date('r') }}">{{ topic.createdTime|time_diff }}</time>
|
||||
</div>
|
||||
|
||||
{% set pages = topic.pageCount %}
|
||||
|
||||
{% if pages > 1 %}
|
||||
<div class="forum__topic__pagination">
|
||||
{% for i in 1..pages|clamp(0, 3) %}
|
||||
<a href="{{ url('forum-topic', {'topic': topic.id, 'page': i}) }}" class="forum__topic__pagination__item">
|
||||
{{ i }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if pages > 3 %}
|
||||
{% if pages > 6 %}
|
||||
<div class="forum__topic__pagination__separator">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for i in (pages - 2)|clamp(4, pages)..pages %}
|
||||
<a href="{{ url('forum-topic', {'topic': topic.id, 'page': i}) }}" class="forum__topic__pagination__item">
|
||||
{{ i }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="forum__topic__stats">
|
||||
<div class="forum__topic__stat" title="Posts">{{ topic.postCount|number_format }}</div>
|
||||
<div class="forum__topic__stat" title="Views">{{ topic.viewCount|number_format }}</div>
|
||||
</div>
|
||||
|
||||
<div class="forum__topic__activity">
|
||||
{% set post = topic.lastPost %}
|
||||
{% if post is not null %}
|
||||
<div class="forum__topic__activity__details">
|
||||
{% if post.user is not empty %}
|
||||
<a href="{{ url('user-profile', {'user': post.user.id}) }}" class="forum__topic__username"
|
||||
style="--user-colour: {{ post.user.colour }}">{{ post.user.username }}</a>
|
||||
{% endif %}
|
||||
|
||||
<a class="forum__topic__activity__post"
|
||||
href="{{ url('forum-post', {'post': post.id, 'post_fragment': 'p' ~ post.id}) }}">
|
||||
<time datetime="{{ post.createdTime|date('c') }}"
|
||||
title="{{ post.createdTime|date('r') }}">{{ post.createdTime|time_diff }}</time>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if post.user is not empty %}
|
||||
<a href="{{ url('user-profile', {'user': post.user.id}) }}" class="forum__topic__avatar">
|
||||
{{ avatar(post.user.id, 30, post.user.username) }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_topic_entry_old(topic, topic_icon, topic_unread) %}
|
||||
{% from 'macros.twig' import avatar %}
|
||||
{% set topic_unread = topic_unread|default(topic.topic_unread|default(false)) %}
|
||||
{% set topic_important = topic.topic_type == constant('MSZ_TOPIC_TYPE_STICKY') or topic.topic_type == constant('MSZ_TOPIC_TYPE_ANNOUNCEMENT') or topic.topic_type == constant('MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT') %}
|
||||
{% set has_priority_voting = forum_has_priority_voting(topic.forum_type) %}
|
||||
{% set topic_important = topic.topic_type == constant('\\Misuzu\\Forum\\ForumTopic::TYPE_STICKY') or topic.topic_type == constant('\\Misuzu\\Forum\\ForumTopic::TYPE_ANNOUNCEMENT') or topic.topic_type == constant('\\Misuzu\\Forum\\ForumTopic::TYPE_GLOBAL_ANNOUNCEMENT') %}
|
||||
{% set has_priority_voting = topic.forum_type in constant('\\Misuzu\\Forum\\ForumCategory::HAS_PRIORITY_VOTES') %}
|
||||
|
||||
{% if topic_icon is null %}
|
||||
{% if topic.topic_deleted is defined and topic.topic_deleted is not null %}
|
||||
{% set topic_icon = 'fas fa-trash-alt' %}
|
||||
{% elseif topic.topic_type is defined and topic.topic_type != constant('MSZ_TOPIC_TYPE_DISCUSSION') %}
|
||||
{% if topic.topic_type == constant('MSZ_TOPIC_TYPE_ANNOUNCEMENT') or topic.topic_type == constant('MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT') %}
|
||||
{% elseif topic.topic_type is defined and topic.topic_type != constant('\\Misuzu\\Forum\\ForumTopic::DISCUSSION') %}
|
||||
{% if topic.topic_type == constant('\\Misuzu\\Forum\\ForumTopic::TYPE_ANNOUNCEMENT') or topic.topic_type == constant('\\Misuzu\\Forum\\ForumTopic::GLOBAL_ANNOUNCEMENT') %}
|
||||
{% set topic_icon = 'fas fa-bullhorn' %}
|
||||
{% elseif topic.topic_type == constant('MSZ_TOPIC_TYPE_STICKY') %}
|
||||
{% elseif topic.topic_type == constant('\\Misuzu\\Forum\\ForumTopic::STICKY') %}
|
||||
{% set topic_icon = 'fas fa-thumbtack' %}
|
||||
{% endif %}
|
||||
{% elseif topic.topic_locked is defined and topic.topic_locked is not null %}
|
||||
|
@ -373,6 +608,114 @@
|
|||
{% endmacro %}
|
||||
|
||||
{% macro forum_post_entry(post, user_id, perms) %}
|
||||
{% if post.getId is defined %}
|
||||
{% from _self import forum_post_entry_new %}
|
||||
{{ forum_post_entry_new(post, user_id, perms) }}
|
||||
{% else %}
|
||||
{% from _self import forum_post_entry_old %}
|
||||
{{ forum_post_entry_old(post, user_id, perms) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_post_entry_new(post, user, perms) %}
|
||||
{% from 'macros.twig' import avatar %}
|
||||
{% 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 (
|
||||
perms|perms_check(constant('MSZ_FORUM_PERM_EDIT_POST'))
|
||||
and post.canBeEdited(user)
|
||||
) %}
|
||||
{% set can_delete = not post.isOpeningPost and (
|
||||
perms|perms_check(constant('MSZ_FORUM_PERM_DELETE_ANY_POST')) or (
|
||||
perms|perms_check(constant('MSZ_FORUM_PERM_DELETE_POST'))
|
||||
and post.canBeDeleted(user)
|
||||
)
|
||||
) %}
|
||||
|
||||
<div class="container forum__post{% if post.deleted %} forum__post--deleted{% endif %}" id="p{{ post.id }}" style="{% if post.hasUser %}--accent-colour: {{ post.user.colour }}{% endif %}">
|
||||
<div class="forum__post__info">
|
||||
<div class="forum__post__info__background"></div>
|
||||
<div class="forum__post__info__content">
|
||||
{% if post.hasUser %}
|
||||
<a class="forum__post__avatar" href="{{ url('user-profile', {'user': post.user.id}) }}">
|
||||
{{ avatar(post.user.id, 120, post.user.username) }}
|
||||
</a>
|
||||
|
||||
<a class="forum__post__username" href="{{ url('user-profile', {'user': post.user.id}) }}">{{ post.user.username }}</a>
|
||||
|
||||
{% if post.user.hasTitle %}
|
||||
<div class="forum__post__usertitle">{{ post.user.title }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="forum__post__icons">
|
||||
<div class="flag flag--{{ post.user.country|lower }}" title="{{ post.user.countryName }}"></div>
|
||||
<div class="forum__post__posts-count">{{ post.user.forumPostCount|number_format }} posts</div>
|
||||
</div>
|
||||
|
||||
{% if post.isTopicAuthor %}
|
||||
<div class="forum__post__badge forum__post__badge--original-poster">
|
||||
<div class="forum__post__badge__desktop">Original Poster</div>
|
||||
<div class="forum__post__badge__mobile">OP</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="forum__post__joined">
|
||||
joined <time datetime="{{ post.user.createdTime|date('c') }}" title="{{ post.user.createdTime|date('r') }}">{{ post.user.createdTime|time_diff }}</time>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="forum__post__username">Deleted User</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="forum__post__content">
|
||||
{% set post_link = url(post.isOpeningPost ? 'forum-topic' : 'forum-post', {'topic': post.topic.id, 'post': post.id, 'post_fragment': 'p%d'|format(post.id)}) %}
|
||||
|
||||
<div class="forum__post__details">
|
||||
<a class="forum__post__datetime" href="{{ post_link }}">
|
||||
<time datetime="{{ post.createdTime|date('c') }}" title="{{ post.createdTime|date('r') }}">{{ post.createdTime|time_diff }}</time>
|
||||
{% if post.isEdited %}
|
||||
(edited <time datetime="{{ post.editedTime|date('c') }}" title="{{ post.editedTime|date('r') }}">{{ post.editedTime|time_diff }}</time>)
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
<a class="forum__post__id" href="{{ post_link }}">
|
||||
#{{ post.id }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="forum__post__text {{ post.bodyClasses }}">
|
||||
{{ post.parsedBody|raw }}
|
||||
</div>
|
||||
|
||||
{% if can_post or can_edit or can_delete %}
|
||||
<div class="forum__post__actions">
|
||||
{% if post.deleted %}
|
||||
<a href="{{ url('forum-post-restore', {'post': post.id}) }}" class="forum__post__action forum__post__action--restore"><i class="fas fa-magic fa-fw"></i> Restore</a>
|
||||
<a href="{{ url('forum-post-nuke', {'post': post.id}) }}" class="forum__post__action forum__post__action--nuke"><i class="fas fa-radiation-alt fa-fw"></i> Permanently Delete</a>
|
||||
{% else %}
|
||||
{# if can_post %}
|
||||
<a href="{{ url('forum-post-quote', {'post': post.id}) }}" class="forum__post__action forum__post__action--quote"><i class="fas fa-quote-left fa-fw"></i> Quote</a>
|
||||
{% endif #}
|
||||
{% if can_edit %}
|
||||
<a href="{{ url('forum-post-edit', {'post': post.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>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if post.shouldDisplaySignature and post.user.hasForumSignature %}
|
||||
<div class="forum__post__signature {{ post.user.forumSignatureClasses }}">
|
||||
{{ post.user.forumSignatureParsed|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_post_entry_old(post, user_id, perms) %}
|
||||
{% from 'macros.twig' import avatar %}
|
||||
{% set is_deleted = post.post_deleted is not null %}
|
||||
{% set can_post = perms|perms_check(constant('MSZ_FORUM_PERM_CREATE_POST')) %}
|
||||
|
@ -384,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')
|
||||
)
|
||||
) %}
|
||||
|
||||
|
@ -472,116 +815,111 @@
|
|||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_poll(poll, options, user_answers, topic_id, can_vote, preview_results) %}
|
||||
{% macro forum_poll(poll, user) %}
|
||||
{% from '_layout/input.twig' import input_csrf, input_hidden, input_checkbox, input_checkbox_raw %}
|
||||
{% set user_answers = user_answers is empty or user_answers is not iterable ? [] : user_answers %}
|
||||
{% set user_answered = user_answers|length > 0 %}
|
||||
{% set results_available = preview_results or user_answered or poll.poll_expired or poll.poll_preview_results %}
|
||||
{% set options_available = not poll.poll_expired and (poll.poll_change_vote or not user_answered) %}
|
||||
{% set display_results = user_answered or poll.poll_expired %}
|
||||
|
||||
{% if options is iterable and options|length > 0 %}
|
||||
<div class="forum__poll">
|
||||
{% if results_available %}
|
||||
{% if options_available %}
|
||||
{{ input_checkbox_raw('', display_results, 'forum__poll__toggle', '', false, {'id':'forum-poll-toggle'}) }}
|
||||
{% set can_vote = poll.canVoteOnPoll(user) %}
|
||||
{% set results_available = poll.hasVoted(user) or poll.hasExpired or poll.canPreviewResults %}
|
||||
{% set options_available = not poll.hasExpired and (poll.canChangeVote or not poll.hasVoted(user)) %}
|
||||
{% set display_results = poll.hasVoted(user) or poll.hasExpired %}
|
||||
|
||||
<div class="forum__poll">
|
||||
{% if results_available %}
|
||||
{% if options_available %}
|
||||
{{ input_checkbox_raw('', display_results, 'forum__poll__toggle', '', false, {'id':'forum-poll-toggle'}) }}
|
||||
{% endif %}
|
||||
|
||||
<div class="container forum__poll__container forum__poll__container--results">
|
||||
<div class="forum__poll__results">
|
||||
{% for option in poll.options %}
|
||||
<div class="forum__poll__result{% if option.hasVotedFor(user) %} forum__poll__result--voted{% endif %}">
|
||||
<div class="forum__poll__result__background" style="width: {{ option.percentage * 100 }}%"></div>
|
||||
<div class="forum__poll__result__container">
|
||||
<div class="forum__poll__result__text">{{ option.text }}</div>
|
||||
<div class="forum__poll__result__votes">{{ option.votes|number_format }}</div>
|
||||
<div class="forum__poll__result__percent">{{ (option.percentage * 100)|number_format(2) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="forum__poll__remaining">
|
||||
This poll got <span class="forum__poll__remaining__num">{{ poll.votes|number_format }} vote{{ poll.votes == 1 ? '' : 's' }}</span>
|
||||
</div>
|
||||
|
||||
{% if poll.canExpire %}
|
||||
<div class="forum__poll__expires">
|
||||
Polling {{ poll.hasExpired ? 'closed' : 'will close' }} <time class="forum__poll__expires__datetime" datetime="{{ poll.expiresTime|date('c') }}" title="{{ poll.expiresTime|date('r') }}">{{ poll.expiresTime|time_diff }}</time>.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="container forum__poll__container forum__poll__container--results">
|
||||
<div class="forum__poll__results">
|
||||
{% for option in options %}
|
||||
{% set percent = poll.poll_votes < 1 ? 0 : (option.option_votes / poll.poll_votes) * 100 %}
|
||||
|
||||
<div class="forum__poll__result{% if option.option_id in user_answers %} forum__poll__result--voted{% endif %}">
|
||||
<div class="forum__poll__result__background" style="width: {{ percent }}%">
|
||||
</div>
|
||||
<div class="forum__poll__result__container">
|
||||
<div class="forum__poll__result__text">{{ option.option_text }}</div>
|
||||
<div class="forum__poll__result__votes">{{ option.option_votes|number_format }}</div>
|
||||
<div class="forum__poll__result__percent">{{ percent|number_format(2) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if options_available %}
|
||||
<div class="forum__poll__buttons">
|
||||
<label class="input__button forum__poll__button" for="forum-poll-toggle">Vote</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="forum__poll__remaining">
|
||||
This poll got <span class="forum__poll__remaining__num">{{ poll.poll_votes|number_format }} vote{{ poll.poll_votes == 1 ? '' : 's' }}</span>
|
||||
{% if options_available %}
|
||||
<form method="post" action="{{ url('forum-poll-vote') }}" class="container forum__poll__container forum__poll__container--poll js-forum-poll"
|
||||
data-poll-id="{{ poll.id }}" data-poll-max-votes="{{ poll.maxVotes }}">
|
||||
{{ input_csrf() }}
|
||||
{{ input_hidden('poll[id]', poll.id) }}
|
||||
|
||||
<div class="forum__poll__options">
|
||||
{% for option in poll.options %}
|
||||
{{ input_checkbox(
|
||||
'poll[answers][]',
|
||||
option.text, option.hasVotedFor(user), 'forum__poll__option',
|
||||
option.id, poll.maxVotes <= 1,
|
||||
null, not can_vote
|
||||
) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if can_vote and poll.maxVotes > 1 %}
|
||||
<div class="forum__poll__remaining js-forum-poll-remaining">
|
||||
You have <span class="forum__poll__remaining__num">
|
||||
<span class="js-forum-poll-remaining-count">{{ poll.maxVotes }}</span> vote<span class="js-forum-poll-remaining-plural">s</span>
|
||||
</span> remaining.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if poll.poll_expires is not null %}
|
||||
<div class="forum__poll__expires">
|
||||
Polling {{ poll.poll_expired ? 'closed' : 'will close' }} <time class="forum__poll__expires__datetime" datetime="{{ poll.poll_expires|date('c') }}" title="{{ poll.poll_expires|date('r') }}">{{ poll.poll_expires|time_diff }}</time>.
|
||||
</div>
|
||||
{% if poll.canExpire %}
|
||||
<div class="forum__poll__expires">
|
||||
Polling {{ poll.hasExpired ? 'closed' : 'will close' }} <time class="forum__poll__expires__datetime" datetime="{{ poll.expiresTime|date('c') }}" title="{{ poll.expiresTime|date('r') }}">{{ poll.expiresTime|time_diff }}</time>.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="forum__poll__buttons">
|
||||
{% if can_vote %}
|
||||
<button class="input__button forum__poll__button">Vote</button>
|
||||
{% endif %}
|
||||
|
||||
{% if options_available %}
|
||||
<div class="forum__poll__buttons">
|
||||
<label class="input__button forum__poll__button" for="forum-poll-toggle">Vote</label>
|
||||
</div>
|
||||
{% if results_available %}
|
||||
<label class="input__button forum__poll__button" for="forum-poll-toggle">Results</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if options_available %}
|
||||
<form method="post" action="{{ url('forum-poll-vote') }}" class="container forum__poll__container forum__poll__container--poll js-forum-poll"
|
||||
data-poll-id="{{ poll.poll_id }}" data-poll-max-votes="{{ poll.poll_max_votes }}">
|
||||
{{ input_csrf() }}
|
||||
{{ input_hidden('poll[id]', poll.poll_id) }}
|
||||
|
||||
<div class="forum__poll__options">
|
||||
{% for option in options %}
|
||||
{{ input_checkbox(
|
||||
'poll[answers][]',
|
||||
option.option_text, option.option_id in user_answers, 'forum__poll__option',
|
||||
option.option_id, poll.poll_max_votes <= 1,
|
||||
null, not can_vote
|
||||
) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if can_vote and poll.poll_max_votes > 1 %}
|
||||
<div class="forum__poll__remaining js-forum-poll-remaining">
|
||||
You have <span class="forum__poll__remaining__num">
|
||||
<span class="js-forum-poll-remaining-count">{{ poll.poll_max_votes }}</span> vote<span class="js-forum-poll-remaining-plural">s</span>
|
||||
</span> remaining.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if poll.poll_expires is not null %}
|
||||
<div class="forum__poll__expires">
|
||||
Polling {{ poll.poll_expired ? 'closed' : 'will close' }} <time class="forum__poll__expires__datetime" datetime="{{ poll.poll_expires|date('c') }}" title="{{ poll.poll_expires|date('r') }}">{{ poll.poll_expires|time_diff }}</time>.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="forum__poll__buttons">
|
||||
{% if can_vote %}
|
||||
<button class="input__button forum__poll__button">Vote</button>
|
||||
{% endif %}
|
||||
{% if results_available %}
|
||||
<label class="input__button forum__poll__button" for="forum-poll-toggle">Results</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_priority_votes(topic, votes, can_vote) %}
|
||||
{% macro forum_priority_votes(topic, user) %}
|
||||
<div class="container forum__priority">
|
||||
<div class="forum__priority__votes">
|
||||
{% for vote in votes %}
|
||||
<div title="{{ vote.username }} ({{ vote.topic_priority|number_format }})" class="forum__priority__vote" style="{{ vote.user_colour|html_colour }}">
|
||||
{% for i in 1..vote.topic_priority %}
|
||||
{% for vote in topic.priorityVotes %}
|
||||
<div title="{{ vote.user.username }} ({{ vote.priority|number_format }})" class="forum__priority__vote" style="--user-colour: {{ vote.user.colour }}">
|
||||
{% for i in 1..vote.priority %}
|
||||
<span class="forum__priority__star fas fa-star fa-fw"></span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if can_vote %}
|
||||
{% if topic.canVoteOnPriority(user) %}
|
||||
<div class="forum__priority__input">
|
||||
<a class="input__button" href="{{ url('forum-topic-priority', {'topic':topic.topic_id}) }}">
|
||||
<a class="input__button" href="{{ url('forum-topic-priority', {'topic': topic.id}) }}">
|
||||
Vote for this feature
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -1 +1,9 @@
|
|||
{% extends 'master.twig' %}
|
||||
|
||||
{% if forum_info is not defined and topic_info.category is defined %}
|
||||
{% set forum_info = topic_info.category %}
|
||||
{% endif %}
|
||||
|
||||
{% if forum_info.colour.raw is defined %}
|
||||
{% set global_accent_colour = forum_info.colour %}
|
||||
{% endif %}
|
||||
|
|
|
@ -5,32 +5,34 @@
|
|||
|
||||
{% 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') }}">
|
||||
{{ input_hidden('post[' ~ (is_reply ? 'topic' : 'forum') ~ ']', is_reply ? posting_topic.topic_id : posting_forum.forum_id) }}
|
||||
{{ input_hidden('post[' ~ (is_reply ? 'topic' : 'forum') ~ ']', is_reply ? posting_topic.id : posting_forum.id) }}
|
||||
{{ input_hidden('post[mode]', posting_mode) }}
|
||||
{{ input_csrf() }}
|
||||
{{ forum_header(
|
||||
posting_forum,
|
||||
true,
|
||||
is_reply and not is_opening
|
||||
? posting_topic.topic_title
|
||||
? url('forum-topic', {'topic': posting_topic.id})
|
||||
: '',
|
||||
[],
|
||||
true,
|
||||
is_reply and not is_opening
|
||||
? posting_topic.title
|
||||
: input_text(
|
||||
'post[title]',
|
||||
'forum__header__input',
|
||||
posting_defaults.title|default(posting_topic.topic_title|default('')),
|
||||
posting_defaults.title|default(posting_topic.title|default('')),
|
||||
'text',
|
||||
'Enter your title here...'
|
||||
),
|
||||
posting_breadcrumbs,
|
||||
false,
|
||||
is_reply and not is_opening
|
||||
? url('forum-topic', {'topic': posting_topic.topic_id})
|
||||
: ''
|
||||
)
|
||||
) }}
|
||||
|
||||
{% 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 %}
|
||||
|
@ -43,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_info.user_country)|lower }}" title="{{ posting_post.poster_country|default(posting_info.user_country)|country_name }}"></div>
|
||||
<div class="forum__post__posts-count">{{ posting_post.poster_post_count|default(posting_info.user_forum_posts)|number_format }} posts</div>
|
||||
<div class="flag flag--{{ posting_post.user.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_info.user_created)|date('c') }}" title="{{ posting_post.poster_joined|default(posting_info.user_created)|date('r') }}">{{ posting_post.poster_joined|default(posting_info.user_created)|time_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>
|
||||
|
@ -75,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>
|
||||
|
@ -140,14 +142,14 @@
|
|||
{{ input_select(
|
||||
'post[parser]',
|
||||
constant('\\Misuzu\\Parsers\\Parser::NAMES'),
|
||||
posting_defaults.parser|default(posting_post.post_parse|default(posting_info.user_post_parse|default(constant('\\Misuzu\\Parsers\\Parser::BBCODE')))),
|
||||
posting_defaults.parser|default(posting_post.bodyParser|default(posting_user.preferredParser)),
|
||||
null, null, false, 'forum__post__dropdown js-forum-posting-parser'
|
||||
) }}
|
||||
{% if is_opening and posting_types|length > 1 %}
|
||||
{{ input_select(
|
||||
'post[type]',
|
||||
posting_types,
|
||||
posting_defaults.type|default(posting_topic.topic_type|default(posting_types|keys|first)),
|
||||
posting_defaults.type|default(posting_topic.type|default(posting_types|keys|first)),
|
||||
null, null, null, 'forum__post__dropdown'
|
||||
) }}
|
||||
{% endif %}
|
||||
|
@ -156,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
|
||||
)
|
||||
) }}
|
||||
|
|
|
@ -12,58 +12,60 @@
|
|||
forum_priority_votes
|
||||
%}
|
||||
|
||||
{% set title = topic_info.topic_title %}
|
||||
{% set title = topic_info.title %}
|
||||
{% set canonical_url = url('forum-topic', {
|
||||
'topic': topic_info.topic_id,
|
||||
'topic': topic_info.id,
|
||||
'page': topic_pagination.page > 1 ? topic_pagination.page : 0,
|
||||
}) %}
|
||||
|
||||
{% set forum_post_csrf = csrf_token() %}
|
||||
{% set topic_tools = forum_topic_tools(topic_info, topic_pagination, can_reply) %}
|
||||
{% set topic_notice = forum_topic_locked(topic_info.topic_locked, topic_info.topic_archived) %}
|
||||
{% set topic_notice = forum_topic_locked(topic_info.lockedTime, topic_info.archived) %}
|
||||
{% set topic_actions = [
|
||||
{
|
||||
'html': '<i class="far fa-trash-alt fa-fw"></i> Delete',
|
||||
'url': url('forum-topic-delete', {'topic': topic_info.topic_id}),
|
||||
'url': url('forum-topic-delete', {'topic': topic_info.id}),
|
||||
'display': topic_can_delete,
|
||||
},
|
||||
{
|
||||
'html': '<i class="fas fa-magic fa-fw"></i> Restore',
|
||||
'url': url('forum-topic-restore', {'topic': topic_info.topic_id}),
|
||||
'url': url('forum-topic-restore', {'topic': topic_info.id}),
|
||||
'display': topic_can_nuke_or_restore,
|
||||
},
|
||||
{
|
||||
'html': '<i class="fas fa-radiation-alt fa-fw"></i> Permanently Delete',
|
||||
'url': url('forum-topic-nuke', {'topic': topic_info.topic_id}),
|
||||
'url': url('forum-topic-nuke', {'topic': topic_info.id}),
|
||||
'display': topic_can_nuke_or_restore,
|
||||
},
|
||||
{
|
||||
'html': '<i class="fas fa-plus-circle fa-fw"></i> Bump',
|
||||
'url': url('forum-topic-bump', {'topic': topic_info.topic_id}),
|
||||
'url': url('forum-topic-bump', {'topic': topic_info.id}),
|
||||
'display': topic_can_bump,
|
||||
},
|
||||
{
|
||||
'html': '<i class="fas fa-lock fa-fw"></i> Lock',
|
||||
'url': url('forum-topic-lock', {'topic': topic_info.topic_id}),
|
||||
'display': topic_can_lock and topic_info.topic_locked is null,
|
||||
'url': url('forum-topic-lock', {'topic': topic_info.id}),
|
||||
'display': topic_can_lock and not topic_info.locked,
|
||||
},
|
||||
{
|
||||
'html': '<i class="fas fa-lock-open fa-fw"></i> Unlock',
|
||||
'url': url('forum-topic-unlock', {'topic': topic_info.topic_id}),
|
||||
'display': topic_can_lock and topic_info.topic_locked is not null,
|
||||
'url': url('forum-topic-unlock', {'topic': topic_info.id}),
|
||||
'display': topic_can_lock and topic_info.locked,
|
||||
},
|
||||
] %}
|
||||
|
||||
{% block content %}
|
||||
{{ forum_header(topic_info.topic_title, topic_breadcrumbs, false, canonical_url, topic_actions) }}
|
||||
{{ forum_header(topic_info.category, true, canonical_url, topic_actions, true, topic_info.title) }}
|
||||
{{ topic_notice }}
|
||||
{% if forum_has_priority_voting(topic_info.forum_type) %}
|
||||
{{ forum_priority_votes(topic_info, topic_priority_votes, true) }} {# replace true this with perms check #}
|
||||
{% if topic_info.hasPriorityVoting %}
|
||||
{{ forum_priority_votes(topic_info, current_user|default(null)) }}
|
||||
{% endif %}
|
||||
{{ forum_poll(topic_info, topic_poll_options, topic_poll_user_answers, topic_info.topic_id, current_user.id|default(0) > 0, topic_info.author_user_id == current_user.id|default(0)) }}
|
||||
{% for poll in topic_info.polls %}
|
||||
{{ forum_poll(poll, current_user|default(null)) }}
|
||||
{% endfor %}
|
||||
{{ topic_tools }}
|
||||
{{ forum_post_listing(topic_posts, current_user.id|default(0), topic_perms) }}
|
||||
{{ forum_post_listing(topic_info.posts(topic_can_view_deleted, topic_pagination), current_user|default(null), topic_perms) }}
|
||||
{{ topic_tools }}
|
||||
{{ topic_notice }}
|
||||
{{ forum_header('', topic_breadcrumbs) }}
|
||||
{{ forum_header(topic_info.category, false) }}
|
||||
{% endblock %}
|
||||
|
|
|
@ -11,63 +11,6 @@
|
|||
</ul>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro pagination(info, path, page_range, params, page_param, url_fragment) %}
|
||||
{% if info.page is defined and info.pages > 1 %}
|
||||
{% set params = params is iterable ? params : [] %}
|
||||
{% set page_param = page_param|default('p') %}
|
||||
{% set page_range = page_range|default(5) %}
|
||||
|
||||
<div class="pagination">
|
||||
<div class="pagination__section pagination__section--first">
|
||||
{% if info.page <= 1 %}
|
||||
<div class="pagination__link pagination__link--first pagination__link--disabled">
|
||||
<i class="fas fa-angle-double-left"></i>
|
||||
</div>
|
||||
<div class="pagination__link pagination__link--prev pagination__link--disabled">
|
||||
<i class="fas fa-angle-left"></i>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{{ url_construct(path, params, url_fragment) }}" class="pagination__link pagination__link--first" rel="first">
|
||||
<i class="fas fa-angle-double-left"></i>
|
||||
</a>
|
||||
<a href="{{ url_construct(path, info.page <= 2 ? params : params|merge({(page_param): info.page - 1}), url_fragment) }}" class="pagination__link pagination__link--prev" rel="prev">
|
||||
<i class="fas fa-angle-left"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="pagination__section pagination__section--pages">
|
||||
{% set p_start = max(info.page - page_range, 1) %}
|
||||
{% set p_stop = min(info.page + page_range, info.pages) %}
|
||||
|
||||
{% for i in p_start..p_stop %}
|
||||
<a href="{{ url_construct(path, i <= 1 ? params : params|merge({(page_param): i}), url_fragment) }}" class="pagination__link{{ info.page == i ? ' pagination__link--current' : '' }}">
|
||||
{{ i }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="pagination__section pagination__section--last">
|
||||
{% if info.page >= info.pages %}
|
||||
<div class="pagination__link pagination__link--next pagination__link--disabled">
|
||||
<i class="fas fa-angle-right"></i>
|
||||
</div>
|
||||
<div class="pagination__link pagination__link--last pagination__link--disabled">
|
||||
<i class="fas fa-angle-double-right"></i>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{{ url_construct(path, params|merge({(page_param): info.page + 1}), url_fragment) }}" class="pagination__link pagination__link--next" rel="next">
|
||||
<i class="fas fa-angle-right"></i>
|
||||
</a>
|
||||
<a href="{{ url_construct(path, params|merge({(page_param): info.pages}), url_fragment) }}" class="pagination__link pagination__link--last" rel="last">
|
||||
<i class="fas fa-angle-double-right"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro container_title(title, unsafe, url) %}
|
||||
{% set has_url = url is not null and url|length > 0 %}
|
||||
|
||||
|
@ -86,5 +29,5 @@
|
|||
{% endmacro %}
|
||||
|
||||
{% macro avatar(user_id, resolution, alt_text, attributes) %}
|
||||
{{ html_avatar(user_id, resolution, alt_text|default(''), attributes|default([]))|raw }}
|
||||
<img src="{{ url('user-avatar', {'user': user_id|default(0), 'res': resolution|default(0) * 2}) }}" alt="{{ alt_text|default('') }}" class="{{ ('avatar ' ~ attributes.class|default(''))|trim }}"{% if resolution > 0 %} width="{{ resolution }}" height="{{ resolution }}"{% endif %}/>
|
||||
{% endmacro %}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends 'manage/changelog/master.twig' %}
|
||||
{% from 'macros.twig' import pagination, container_title %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
{% from 'changelog/macros.twig' import changelog_listing %}
|
||||
|
||||
{% block manage_content %}
|
||||
{% set changelog_pagination = pagination(changelog_pagination, url('manage-changelog-changes')) %}
|
||||
{% set changelog_pagination = changelog_pagination.render('manage-changelog-changes') %}
|
||||
|
||||
|
||||
<div class="container">
|
||||
|
@ -12,13 +12,13 @@
|
|||
<div class="changelog__content">
|
||||
<div class="changelog__pagination">
|
||||
<a href="{{ url('manage-changelog-change') }}" class="input__button">Create new change</a>
|
||||
{{ changelog_pagination }}
|
||||
{{ changelog_pagination|raw }}
|
||||
</div>
|
||||
|
||||
{{ changelog_listing(changelog_changes, false, false, true) }}
|
||||
|
||||
<div class="changelog__pagination">
|
||||
{{ changelog_pagination }}
|
||||
{{ changelog_pagination|raw }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
{% extends 'manage/general/master.twig' %}
|
||||
{% from 'macros.twig' import container_title, pagination %}
|
||||
{% from 'macros.twig' import container_title, %}
|
||||
{% from 'user/macros.twig' import user_account_log %}
|
||||
|
||||
{% block manage_content %}
|
||||
<div class="container settings__container">
|
||||
{{ container_title('<i class="fas fa-file-alt fa-fw"></i> Global Log') }}
|
||||
{% set glp = pagination(global_logs_pagination, url('manage-general-logs'), null, {'v': 'logs'}) %}
|
||||
{% set glp = global_logs_pagination.render('manage-general-logs', {'v': 'logs'}) %}
|
||||
|
||||
<div class="settings__account-logs">
|
||||
<div class="settings__account-logs__pagination">
|
||||
{{ glp }}
|
||||
{{ glp|raw }}
|
||||
</div>
|
||||
|
||||
{% for log in global_logs %}
|
||||
|
@ -17,7 +17,7 @@
|
|||
{% endfor %}
|
||||
|
||||
<div class="settings__account-logs__pagination">
|
||||
{{ glp }}
|
||||
{{ glp|raw }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'manage/news/master.twig' %}
|
||||
{% from 'macros.twig' import pagination, container_title %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
|
||||
{% block manage_content %}
|
||||
<div class="container">
|
||||
|
@ -16,6 +16,6 @@
|
|||
</p>
|
||||
{% endfor %}
|
||||
|
||||
{{ pagination(categories_pagination, url('manage-news-categories')) }}
|
||||
{{ categories_pagination.render('manage-news-categories')|raw }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'manage/news/master.twig' %}
|
||||
{% from 'macros.twig' import pagination, container_title %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
|
||||
{% block manage_content %}
|
||||
<div class="container">
|
||||
|
@ -22,6 +22,6 @@
|
|||
</p>
|
||||
{% endfor %}
|
||||
|
||||
{{ pagination(posts_pagination, url('manage-news-posts')) }}
|
||||
{{ posts_pagination.render('manage-news-posts') }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'manage/users/master.twig' %}
|
||||
{% from 'macros.twig' import pagination, container_title %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
|
||||
{% set roles_pagination = pagination(manage_roles_pagination, url('manage-roles')) %}
|
||||
{% set roles_pagination = manage_roles_pagination.render('manage-roles') %}
|
||||
|
||||
{% block manage_content %}
|
||||
<div class="container manage__roles">
|
||||
|
@ -9,7 +9,7 @@
|
|||
|
||||
{% if roles_pagination|trim|length > 0 %}
|
||||
<div class="manage__roles__pagination">
|
||||
{{ roles_pagination }}
|
||||
{{ roles_pagination|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -71,7 +71,7 @@
|
|||
|
||||
{% if roles_pagination|trim|length > 0 %}
|
||||
<div class="manage__roles__pagination">
|
||||
{{ roles_pagination }}
|
||||
{{ roles_pagination|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'manage/users/master.twig' %}
|
||||
{% from 'macros.twig' import pagination, container_title, avatar %}
|
||||
{% from 'macros.twig' import container_title, avatar %}
|
||||
|
||||
{% set users_pagination = pagination(manage_users_pagination, url('manage-users')) %}
|
||||
{% set users_pagination = manage_users_pagination.render('manage-users') %}
|
||||
|
||||
{% block manage_content %}
|
||||
<div class="container manage__users">
|
||||
|
@ -9,7 +9,7 @@
|
|||
|
||||
{% if users_pagination|trim|length > 0 %}
|
||||
<div class="manage__users__pagination">
|
||||
{{ users_pagination }}
|
||||
{{ users_pagination|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -55,7 +55,7 @@
|
|||
|
||||
{% if users_pagination|trim|length > 0 %}
|
||||
<div class="manage__users__pagination">
|
||||
{{ users_pagination }}
|
||||
{{ users_pagination|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'manage/users/master.twig' %}
|
||||
{% from 'macros.twig' import pagination, container_title %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
{% from 'user/macros.twig' import user_profile_warning %}
|
||||
{% from '_layout/input.twig' import input_text, input_csrf, input_select, input_hidden %}
|
||||
|
||||
|
@ -38,9 +38,9 @@
|
|||
|
||||
<div class="container container--lazy">
|
||||
{{ container_title('<i class="fas fa-exclamation-circle fa-fw"></i> Warnings') }}
|
||||
{% set warnpag = pagination(warnings.pagination, url('manage-users-warnings', {'user': warnings.user.id|default(0)})) %}
|
||||
{% set warnpag = warnings.pagination.render('manage-users-warnings', {'user': warnings.user.id|default(0)}) %}
|
||||
|
||||
{{ warnpag }}
|
||||
{{ warnpag|raw }}
|
||||
|
||||
<div class="profile__warnings__container">
|
||||
<div class="profile__warning profile__warning--extendo">
|
||||
|
@ -90,6 +90,6 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{{ warnpag }}
|
||||
{{ warnpag|raw }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
{% endif %}
|
||||
</head>
|
||||
<body class="main{% if site_background is defined %} {{ site_background.classNames('main--bg-%s')|join(' ') }}{% endif %}"
|
||||
style="{% if global_accent_colour is defined %}{{ global_accent_colour|html_colour('--accent-colour') }}{% endif %}" id="container">
|
||||
style="{% if global_accent_colour.raw is defined %}--accent-colour: {{ global_accent_colour }}{% elseif global_accent_colour is defined %}{{ global_accent_colour|html_colour('--accent-colour') }}{% endif %}" id="container">
|
||||
{% include '_layout/header.twig' %}
|
||||
|
||||
<div class="main__wrapper">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'news/master.twig' %}
|
||||
{% from 'macros.twig' import pagination, container_title %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
{% from 'news/macros.twig' import news_preview %}
|
||||
|
||||
{% set title = category_info.name ~ ' :: News' %}
|
||||
|
@ -30,7 +30,7 @@
|
|||
{% endfor %}
|
||||
|
||||
<div class="container" style="padding: 4px; display: {{ news_pagination.pages > 1 ? 'block' : 'none' }}">
|
||||
{{ pagination(news_pagination, url('news-category', {'category':category_info.id})) }}
|
||||
{{ news_pagination.render(url('news-category', {'category':category_info.id}))|raw }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'news/master.twig' %}
|
||||
{% from 'macros.twig' import pagination, container_title %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
{% from 'news/macros.twig' import news_preview %}
|
||||
|
||||
{% set title = 'News' %}
|
||||
|
@ -29,7 +29,7 @@
|
|||
{% endfor %}
|
||||
|
||||
<div class="container" style="padding: 4px; display: {{ news_pagination.pages > 1 ? 'block' : 'none' }}">
|
||||
{{ pagination(news_pagination, url('news-index')) }}
|
||||
{{ news_pagination.render('news-index')|raw }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,23 +1,13 @@
|
|||
{% extends 'profile/master.twig' %}
|
||||
{% from 'macros.twig' import pagination %}
|
||||
{% from 'forum/macros.twig' import forum_post_listing %}
|
||||
|
||||
{% block content %}
|
||||
<div class="profile">
|
||||
{% include 'profile/_layout/header.twig' %}
|
||||
|
||||
{% set sp = profile_posts_pagination.pages > 1
|
||||
? '<div class="container profile__pagination">' ~ pagination(profile_posts_pagination, canonical_url) ~ '</div>'
|
||||
: '' %}
|
||||
|
||||
{% if sp is not empty %}
|
||||
{{ sp|raw }}
|
||||
{% endif %}
|
||||
|
||||
{{ forum_post_listing(profile_posts) }}
|
||||
|
||||
{% if sp is not empty %}
|
||||
{{ sp|raw }}
|
||||
{% endif %}
|
||||
<div class="warning">
|
||||
<div class="warning__content">
|
||||
<p>User post listing is gone for a while, it will be back someday but with less bad.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
{% extends 'profile/master.twig' %}
|
||||
{% from 'macros.twig' import pagination %}
|
||||
{% from 'user/macros.twig' import user_card %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -16,7 +15,7 @@
|
|||
|
||||
{% if profile_relation_pagination.pages > 1 %}
|
||||
<div class="container profile__pagination">
|
||||
{{ pagination(profile_relation_pagination, canonical_url) }}
|
||||
{{ profile_relation_pagination.render(canonical_url)|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -1,23 +1,13 @@
|
|||
{% extends 'profile/master.twig' %}
|
||||
{% from 'macros.twig' import pagination %}
|
||||
{% from 'forum/macros.twig' import forum_topic_listing %}
|
||||
|
||||
{% block content %}
|
||||
<div class="profile">
|
||||
{% include 'profile/_layout/header.twig' %}
|
||||
|
||||
{% set sp = profile_topics_pagination.pages > 1
|
||||
? '<div class="container profile__pagination">' ~ pagination(profile_topics_pagination, canonical_url) ~ '</div>'
|
||||
: '' %}
|
||||
|
||||
{% if sp is not empty %}
|
||||
{{ sp|raw }}
|
||||
{% endif %}
|
||||
|
||||
{{ forum_topic_listing(profile_topics) }}
|
||||
|
||||
{% if sp is not empty %}
|
||||
{{ sp|raw }}
|
||||
{% endif %}
|
||||
<div class="warning">
|
||||
<div class="warning__content">
|
||||
<p>User topic listing is gone for a while, it will be back someday but with less bad.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'settings/master.twig' %}
|
||||
{% from 'macros.twig' import container_title, pagination %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
{% from 'user/macros.twig' import user_login_attempt, user_account_log %}
|
||||
|
||||
{% set title = 'Settings / Logs' %}
|
||||
|
@ -7,7 +7,7 @@
|
|||
{% block settings_content %}
|
||||
<div class="container settings__container" id="login-history">
|
||||
{{ container_title('<i class="fas fa-user-lock fa-fw"></i> Login History') }}
|
||||
{% set lhpagination = pagination(login_history_pagination, url('settings-logs'), null, {
|
||||
{% set lhpagination = login_history_pagination.render('settings-logs', {
|
||||
'ap': account_log_pagination.page > 1 ? account_log_pagination.page : 0,
|
||||
}, 'hp', 'login-history') %}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
|||
|
||||
<div class="settings__login-attempts">
|
||||
<div class="settings__login-attempts__pagination">
|
||||
{{ lhpagination }}
|
||||
{{ lhpagination|raw }}
|
||||
</div>
|
||||
|
||||
{% if login_history_list|length < 1 %}
|
||||
|
@ -31,14 +31,14 @@
|
|||
{% endif %}
|
||||
|
||||
<div class="settings__login-attempts__pagination">
|
||||
{{ lhpagination }}
|
||||
{{ lhpagination|raw }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container settings__container" id="account-log">
|
||||
{{ container_title('<i class="fas fa-file-alt fa-fw"></i> Account Log') }}
|
||||
{% set alpagination = pagination(account_log_pagination, url('settings-logs'), null, {
|
||||
{% set alpagination = account_log_pagination.render('settings-logs', {
|
||||
'hp': login_history_pagination.page > 1 ? login_history_pagination.page : 0,
|
||||
}, 'ap', 'account-log') %}
|
||||
|
||||
|
@ -48,7 +48,7 @@
|
|||
|
||||
<div class="settings__account-logs">
|
||||
<div class="settings__account-logs__pagination">
|
||||
{{ alpagination }}
|
||||
{{ alpagination|raw }}
|
||||
</div>
|
||||
|
||||
{% for log in account_log_list %}
|
||||
|
@ -56,7 +56,7 @@
|
|||
{% endfor %}
|
||||
|
||||
<div class="settings__account-logs__pagination">
|
||||
{{ alpagination }}
|
||||
{{ alpagination|raw }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'settings/master.twig' %}
|
||||
{% from 'macros.twig' import container_title, pagination %}
|
||||
{% from 'macros.twig' import container_title %}
|
||||
{% from 'user/macros.twig' import user_session %}
|
||||
{% from '_layout/input.twig' import input_hidden, input_csrf %}
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
|||
<div class="container settings__container">
|
||||
{{ container_title('<i class="fas fa-key fa-fw"></i> Sessions') }}
|
||||
|
||||
{% set spagination = pagination(session_pagination, url('settings-sessions')) %}
|
||||
{% set spagination = session_pagination.render('settings-sessions') %}
|
||||
|
||||
<div class="settings__description">
|
||||
<p>These are the active logins to your account, clicking the Kill button will force a logout on that session. Your current login is highlighted with a different colour so you don't accidentally force yourself to logout.</p>
|
||||
|
@ -26,7 +26,7 @@
|
|||
</form>
|
||||
|
||||
<div class="settings__sessions__pagination">
|
||||
{{ spagination }}
|
||||
{{ spagination|raw }}
|
||||
</div>
|
||||
|
||||
<div class="settings__sessions__list">
|
||||
|
@ -36,7 +36,7 @@
|
|||
</div>
|
||||
|
||||
<div class="settings__sessions__pagination">
|
||||
{{ spagination }}
|
||||
{{ spagination|raw }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
{% set manage_link = url('manage-users') %}
|
||||
|
||||
{% macro member_nav(roles, role_id, orders, order, directions, direction, users_pagination, url_role, url_sort, url_direction) %}
|
||||
{% from 'macros.twig' import pagination %}
|
||||
{% from '_layout/input.twig' import input_select %}
|
||||
|
||||
<div class="userlist__navigation">
|
||||
|
@ -30,7 +29,7 @@
|
|||
</form>
|
||||
|
||||
<div class="userlist__pagination">
|
||||
{{ pagination(users_pagination, url('user-list'), null, {'r': url_role, 'ss': url_sort, 'sd': url_direction}) }}
|
||||
{{ users_pagination.render('user-list', {'r': url_role, 'ss': url_sort, 'sd': url_direction})|raw }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
|
67
utility.php
67
utility.php
|
@ -33,16 +33,21 @@ function clamp($num, int $min, int $max): int {
|
|||
return max($min, min($max, intval($num)));
|
||||
}
|
||||
|
||||
function starts_with(string $string, string $text, bool $multibyte = true): bool {
|
||||
function starts_with(string $haystack, string $needle, bool $multibyte = true): bool {
|
||||
$strlen = $multibyte ? 'mb_strlen' : 'strlen';
|
||||
$substr = $multibyte ? 'mb_substr' : 'substr';
|
||||
return $substr($string, 0, $strlen($text)) === $text;
|
||||
return $substr($haystack, 0, $strlen($needle)) === $needle;
|
||||
}
|
||||
|
||||
function ends_with(string $string, string $text, bool $multibyte = true): bool {
|
||||
function ends_with(string $haystack, string $needle, bool $multibyte = true): bool {
|
||||
$strlen = $multibyte ? 'mb_strlen' : 'strlen';
|
||||
$substr = $multibyte ? 'mb_substr' : 'substr';
|
||||
return $substr($string, 0 - $strlen($text)) === $text;
|
||||
return $substr($haystack, 0 - $strlen($needle)) === $needle;
|
||||
}
|
||||
|
||||
if(!function_exists('str_starts_with')) {
|
||||
function str_starts_with(string $haystack, string $needle): bool { return starts_with($haystack, $needle, false); }
|
||||
function str_ends_with(string $haystack, string $needle): bool { return ends_with($haystack, $needle, false); }
|
||||
}
|
||||
|
||||
function first_paragraph(string $text, string $delimiter = "\n"): string {
|
||||
|
@ -79,22 +84,14 @@ function byte_symbol(int $bytes, bool $decimal = false, array $symbols = ['', 'K
|
|||
return sprintf("%.2f %s%sB", $bytes, $symbol, $symbol !== '' && !$decimal ? 'i' : '');
|
||||
}
|
||||
|
||||
function get_country_name(string $code): string {
|
||||
switch(strtolower($code)) {
|
||||
case 'xx':
|
||||
return 'Unknown';
|
||||
|
||||
case 'a1':
|
||||
return 'Anonymous Proxy';
|
||||
|
||||
case 'a2':
|
||||
return 'Satellite Provider';
|
||||
|
||||
default:
|
||||
return locale_get_display_region("-{$code}", 'en');
|
||||
}
|
||||
function get_country_name(string $code, string $locale = 'en'): string {
|
||||
if($code === 'xx')
|
||||
return 'Unknown';
|
||||
return \Locale::getDisplayRegion("-{$code}", $locale);
|
||||
}
|
||||
|
||||
/*** THE DEPRECATION NATION (LINE) ***/
|
||||
|
||||
// render_error, render_info and render_info_or_json should be redone a bit better
|
||||
// following a uniform format so there can be a global handler for em
|
||||
|
||||
|
@ -161,37 +158,3 @@ function html_colour(?int $colour, $attribs = '--user-colour'): string {
|
|||
|
||||
return $css;
|
||||
}
|
||||
|
||||
function html_avatar(?int $userId, int $resolution, string $altText = '', array $attributes = []): string {
|
||||
$attributes['src'] = url('user-avatar', ['user' => $userId ?? 0, 'res' => $resolution * 2]);
|
||||
$attributes['alt'] = $altText;
|
||||
$attributes['class'] = trim('avatar ' . ($attributes['class'] ?? ''));
|
||||
|
||||
if(!isset($attributes['width']))
|
||||
$attributes['width'] = $resolution;
|
||||
if(!isset($attributes['height']))
|
||||
$attributes['height'] = $resolution;
|
||||
|
||||
return html_tag('img', $attributes);
|
||||
}
|
||||
|
||||
function html_tag(string $name, array $atrributes = [], ?bool $close = null, string $content = ''): string {
|
||||
$html = '<' . $name;
|
||||
|
||||
foreach($atrributes as $key => $value) {
|
||||
$html .= ' ' . $key;
|
||||
|
||||
if(!empty($value))
|
||||
$html .= '="' . $value . '"';
|
||||
}
|
||||
|
||||
if($close === false)
|
||||
$html .= '/';
|
||||
|
||||
$html .= '>';
|
||||
|
||||
if($close === true)
|
||||
$html .= $content . '</' . $name . '>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue