//further progress

This commit is contained in:
flash 2020-10-21 00:58:36 +00:00
parent 7ea5e9414d
commit ceb05fc3f7
15 changed files with 373 additions and 651 deletions

View file

@ -77,10 +77,8 @@ 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/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);

View file

@ -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';

View file

@ -5,6 +5,10 @@ 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\ForumPostNotFoundException;
use Misuzu\Net\IPAddress;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
@ -130,6 +134,7 @@ if($mode === 'edit') {
}
$notices = [];
$isNewTopic = false;
if(!empty($_POST)) {
$topicTitle = $_POST['post']['title'] ?? '';
@ -141,7 +146,7 @@ if(!empty($_POST)) {
if(!CSRF::validateRequest()) {
$notices[] = 'Could not verify request.';
} else {
$isEditingTopic = empty($topicInfo) || ($mode === 'edit' && $post['is_opening_post']);
$isEditingTopic = $isNewTopic || ($mode === 'edit' && $post['is_opening_post']);
if($mode === 'create') {
$timeoutCheck = max(1, forum_timeout($forumInfo->getId(), $currentUserId));
@ -153,20 +158,14 @@ if(!empty($_POST)) {
}
if($isEditingTopic) {
$originalTopicTitle = empty($topicInfo) ? null : $topicInfo->getTitle();
$originalTopicTitle = $isNewTopic ? null : $topicInfo->getTitle();
$topicTitleChanged = $topicTitle !== $originalTopicTitle;
$originalTopicType = empty($topicInfo) ? ForumTopic::TYPE_DISCUSSION : $topicInfo->getType();
$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);
@ -175,19 +174,12 @@ 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) {
@ -195,12 +187,9 @@ if(!empty($_POST)) {
if(!empty($topicInfo)) {
$topicInfo->bumpTopic();
} else {
$topicId = forum_topic_create(
$forumInfo->getId(),
$currentUserId,
$topicTitle,
$topicType
);
$isNewTopic = true;
$topicInfo = ForumTopic::create($forumInfo, $currentUser, $topicTitle, $topicType);
$topicId = $topicInfo->getId();
}
$postId = forum_post_create(
@ -213,7 +202,7 @@ if(!empty($_POST)) {
$postSignature
);
forum_topic_mark_read($currentUserId, $topicId, $forumInfo->getId());
$forumInfo->increaseTopicPostCount(empty($topicInfo));
$forumInfo->increaseTopicPostCount($isNewTopic);
break;
case 'edit':
@ -222,7 +211,11 @@ if(!empty($_POST)) {
}
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.';
}
}
@ -230,7 +223,7 @@ if(!empty($_POST)) {
}
if(empty($notices)) {
$redirect = url(empty($topicInfo) ? 'forum-topic' : 'forum-post', [
$redirect = url($isNewTopic ? 'forum-topic' : 'forum-post', [
'topic' => $topicId ?? 0,
'post' => $postId ?? 0,
'post_fragment' => 'p' . ($postId ?? 0),
@ -242,7 +235,7 @@ if(!empty($_POST)) {
}
}
if(!empty($topicInfo)) {
if(!$isNewTopic && !empty($topicInfo)) {
Template::set('posting_topic', $topicInfo);
}

View file

@ -59,7 +59,7 @@ $canNukeOrRestore = $canDeleteAny && $topicInfo->isDeleted();
$canDelete = !$topicInfo->isDeleted() && (
$canDeleteAny || (
$topicPostsTotal > 0
&& $topicPostsTotal <= MSZ_FORUM_TOPIC_DELETE_POST_LIMIT
&& $topicPostsTotal <= ForumTopic::DELETE_POST_LIMIT
&& $canDeleteOwn
&& $topicInfo->getUserId() === $topicUserId
)
@ -104,47 +104,20 @@ if(in_array($moderationMode, $validModerationModes, true)) {
switch($moderationMode) {
case 'delete':
$canDeleteCode = forum_topic_can_delete($topicInfo, $topicUserId);
$canDeleteMsg = '';
$responseCode = 200;
$canDeleteCodes = [
'view' => 404,
'deleted' => 404,
'owner' => 403,
'age' => 403,
'permission' => 403,
'posts' => 403,
'' => 200,
];
$canDelete = $topicInfo->canDelete($topicUser);
$canDeleteMsg = ForumTopic::canDeleteErrorString($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([
@ -181,26 +154,18 @@ if(in_array($moderationMode, $validModerationModes, true)) {
}
}
$deleteTopic = forum_topic_delete($topicInfo->getId());
if($deleteTopic) {
AuditLog::create(AuditLog::FORUM_TOPIC_DELETE, [$topicInfo->getId()]);
}
$topicInfo->delete();
AuditLog::create(AuditLog::FORUM_TOPIC_DELETE, [$topicInfo->getId()]);
if($isXHR) {
echo json_encode([
'success' => $deleteTopic,
'success' => true,
'topic_id' => $topicInfo->getId(),
'message' => $deleteTopic ? 'Topic deleted!' : 'Failed to delete topic.',
'message' => 'Topic deleted!',
]);
break;
}
if(!$deleteTopic) {
echo render_error(500);
break;
}
url_redirect('forum-category', [
'forum' => $topicInfo->getCategoryId(),
]);
@ -232,13 +197,7 @@ if(in_array($moderationMode, $validModerationModes, true)) {
}
}
$restoreTopic = forum_topic_restore($topicInfo->getId());
if(!$restoreTopic) {
echo render_error(500);
break;
}
$topicInfo->restore();
AuditLog::create(AuditLog::FORUM_TOPIC_RESTORE, [$topicInfo->getId()]);
http_response_code(204);
@ -275,13 +234,7 @@ if(in_array($moderationMode, $validModerationModes, true)) {
}
}
$nukeTopic = forum_topic_nuke($topicInfo->getId());
if(!$nukeTopic) {
echo render_error(500);
break;
}
$topicInfo->nuke();
AuditLog::create(AuditLog::FORUM_TOPIC_NUKE, [$topicInfo->getId()]);
http_response_code(204);

View file

@ -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;

View file

@ -414,7 +414,7 @@ class ForumCategory {
}
if($save && !$this->isRoot()) {
$setCounts = \Misuzu\DB::prepare(
$setCounts = DB::prepare(
'UPDATE `msz_forum_categories`'
. ' SET `forum_count_topics` = :topics, `forum_count_posts` = :posts'
. ' WHERE `forum_id` = :forum'

View 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;
}
}

View file

@ -15,6 +15,9 @@ class ForumPostCreationFailedException extends ForumPostException {}
class ForumPost {
public const PER_PAGE = 10;
public const BODY_MIN_LENGTH = 1;
public const BODY_MAX_LENGTH = 60000;
// Database fields
private $post_id = -1;
private $topic_id = -1;
@ -176,11 +179,6 @@ class ForumPost {
public function isDeleted(): bool {
return $this->getDeletedTime() >= 0;
}
public function setDeleted(bool $deleted): self {
if($this->isDeleted() !== $deleted)
$this->post_deleted = $deleted ? time() : null;
return $this;
}
public function isOpeningPost(): bool {
return $this->getTopic()->isOpeningPost($this);
@ -211,6 +209,58 @@ class ForumPost {
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 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();
}
private static function countQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf('COUNT(*)', self::TABLE));
}

View file

@ -8,6 +8,8 @@ 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;
@ -35,6 +37,12 @@ class ForumTopic {
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;
// Database fields
private $topic_id = -1;
private $forum_id = -1;
@ -236,11 +244,6 @@ class ForumTopic {
public function isDeleted(): bool {
return $this->getDeletedTime() >= 0;
}
public function setDeleted(bool $deleted): self {
if($this->isDeleted() !== $deleted)
$this->topic_deleted = $deleted ? time() : null;
return $this;
}
public function getLockedTime(): int {
return $this->topic_locked === null ? -1 : $this->topic_locked;
@ -274,7 +277,7 @@ class ForumTopic {
public function hasUnread(?User $user): bool {
if($user === null)
return false;
return true;
return mt_rand(0, 10) >= 5;
}
public function hasParticipated(?User $user): bool {
@ -304,6 +307,112 @@ class ForumTopic {
return $this->getCategory()->canView($user);
}
public function canDelete(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 canDeleteErrorString(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 `msz_forum_topics`'
. ' 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`, ('
@ -337,6 +446,27 @@ class ForumTopic {
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));
}

View file

@ -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;
}

View file

@ -1,55 +1,4 @@
<?php
function forum_topic_create(
int $forumId,
int $userId,
string $title,
int $type = \Misuzu\Forum\ForumTopic::TYPE_DISCUSSION
): int {
if(empty($title) || !in_array($type, \Misuzu\Forum\ForumTopic::TYPES)) {
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 && !in_array($type, \Misuzu\Forum\ForumTopic::TYPES)) {
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_views_increment(int $topicId): void {
if($topicId < 1) {
return;
@ -72,6 +21,7 @@ function forum_topic_mark_read(int $userId, int $topicId, int $forumId): void {
// previously a TRIGGER was used to achieve this behaviour,
// but those explode when running on a lot of queries (like forum_mark_read() does)
// so instead we get to live with this garbage now
// JUST TO CLARIFY: "this behaviour" refers to forum_topic_views_increment only being executed when the topic is viewed for the first time
try {
$markAsRead = \Misuzu\DB::prepare('
INSERT INTO `msz_forum_topics_track`
@ -104,283 +54,3 @@ function forum_topic_mark_read(int $userId, int $topicId, int $forumId): void {
$markAsRead->execute();
}
}
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), t.`topic_bumped` DESC
%2$s
',
$showDeleted ? '' : 'AND t.`topic_deleted` IS NULL',
$hasPagination ? 'LIMIT :offset, :take' : '',
\Misuzu\Forum\ForumTopic::TYPE_GLOBAL_ANNOUNCEMENT,
implode(',', \Misuzu\Forum\ForumTopic::TYPE_ORDER),
$showDeleted ? '' : 'AND `post_deleted` IS NULL',
\Misuzu\Forum\ForumPost::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();
}
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;
} elseif(is_int($topicId)) {
try {
$topic = \Misuzu\Forum\ForumTopic::byId($topicId);
} catch(\Misuzu\Forum\ForumTopicNotFoundException $ex) {
return MSZ_E_FORUM_TOPIC_DELETE_TOPIC;
}
}
if($topicId instanceof \Misuzu\Forum\ForumTopic) {
$topic = [
'forum_id' => $topicId->getCategoryId(),
'topic_deleted' => $topicId->isDeleted(),
'author_user_id' => $topicId->getUserId(),
'topic_created' => date('c', $topicId->getCreatedTime()),
'topic_count_posts' => $topicId->getActualPostCount(true),
'topic_count_posts_deleted' => 0,
];
}
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();
}

View file

@ -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 '';
}

View file

@ -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 %}

View file

@ -1,22 +1,13 @@
{% extends 'profile/master.twig' %}
{% 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">' ~ profile_posts_pagination.render(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 %}

View file

@ -1,22 +1,13 @@
{% extends 'profile/master.twig' %}
{% 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">' ~ profile_topics_pagination.render(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 %}