Rewrote comments backend.
This commit is contained in:
parent
7b212c2c68
commit
41f1a59b1d
36 changed files with 1380 additions and 799 deletions
assets/css/misuzu/changelog
misuzu.phppublic
src
templates
_layout
changelog
news
profile
6
assets/css/misuzu/changelog/_changelog.css
Normal file
6
assets/css/misuzu/changelog/_changelog.css
Normal file
|
@ -0,0 +1,6 @@
|
|||
.changelog__action--add { --action-colour: #159635 !important; }
|
||||
.changelog__action--remove { --action-colour: #e33743 !important; }
|
||||
.changelog__action--update { --action-colour: #297b8a !important; }
|
||||
.changelog__action--fix { --action-colour: #2d5e96 !important; }
|
||||
.changelog__action--import { --action-colour: #2b9678 !important; }
|
||||
.changelog__action--revert { --action-colour: #e38245 !important; }
|
|
@ -30,12 +30,6 @@
|
|||
.changelog__entry__action__text {
|
||||
width: 100%;
|
||||
}
|
||||
.changelog__action--add { --action-colour: #159635; }
|
||||
.changelog__action--remove { --action-colour: #e33743; }
|
||||
.changelog__action--update { --action-colour: #297b8a; }
|
||||
.changelog__action--fix { --action-colour: #2d5e96; }
|
||||
.changelog__action--import { --action-colour: #2b9678; }
|
||||
.changelog__action--revert { --action-colour: #e38245; }
|
||||
|
||||
.changelog__entry__datetime {
|
||||
min-width: 100px;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
--action-colour: var(--accent-colour);
|
||||
|
||||
border: 1px solid var(--action-colour);
|
||||
background-color: var(--action-colour);
|
||||
background-color: var(--background-colour);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex: 1 0 auto;
|
||||
|
|
|
@ -64,7 +64,6 @@ require_once 'utility.php';
|
|||
require_once 'src/perms.php';
|
||||
require_once 'src/audit_log.php';
|
||||
require_once 'src/changelog.php';
|
||||
require_once 'src/comments.php';
|
||||
require_once 'src/manage.php';
|
||||
require_once 'src/url.php';
|
||||
require_once 'src/Forum/perms.php';
|
||||
|
|
|
@ -3,6 +3,7 @@ namespace Misuzu;
|
|||
|
||||
use Misuzu\Net\IPAddress;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserNotFoundException;
|
||||
|
||||
require_once '../../misuzu.php';
|
||||
|
||||
|
@ -44,7 +45,6 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
break;
|
||||
}
|
||||
|
||||
$userData = User::findForLogin($_POST['login']['username']);
|
||||
$attemptsRemainingError = sprintf(
|
||||
"%d attempt%s remaining",
|
||||
$remainingAttempts - 1,
|
||||
|
@ -52,7 +52,9 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
);
|
||||
$loginFailedError = "Invalid username or password, {$attemptsRemainingError}.";
|
||||
|
||||
if(empty($userData)) {
|
||||
try {
|
||||
$userData = User::findForLogin($_POST['login']['username']);
|
||||
} catch(UserNotFoundException $ex) {
|
||||
user_login_attempt_record(false, null, $ipAddress, $userAgent);
|
||||
$notices[] = $loginFailedError;
|
||||
break;
|
||||
|
@ -64,7 +66,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
}
|
||||
|
||||
if($userData->isDeleted() || !$userData->checkPassword($_POST['login']['password'])) {
|
||||
user_login_attempt_record(false, $userData->user_id, $ipAddress, $userAgent);
|
||||
user_login_attempt_record(false, $userData->getId(), $ipAddress, $userAgent);
|
||||
$notices[] = $loginFailedError;
|
||||
break;
|
||||
}
|
||||
|
@ -73,31 +75,31 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
$userData->setPassword($_POST['login']['password']);
|
||||
}
|
||||
|
||||
if(!empty($loginPermCat) && $loginPermVal > 0 && !perms_check_user($loginPermCat, $userData['user_id'], $loginPermVal)) {
|
||||
if(!empty($loginPermCat) && $loginPermVal > 0 && !perms_check_user($loginPermCat, $userData->getId(), $loginPermVal)) {
|
||||
$notices[] = "Login succeeded, but you're not allowed to browse the site right now.";
|
||||
user_login_attempt_record(true, $userData->user_id, $ipAddress, $userAgent);
|
||||
user_login_attempt_record(true, $userData->getId(), $ipAddress, $userAgent);
|
||||
break;
|
||||
}
|
||||
|
||||
if($userData->hasTOTP()) {
|
||||
url_redirect('auth-two-factor', [
|
||||
'token' => user_auth_tfa_token_create($userData->user_id),
|
||||
'token' => user_auth_tfa_token_create($userData->getId()),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
user_login_attempt_record(true, $userData->user_id, $ipAddress, $userAgent);
|
||||
$sessionKey = user_session_create($userData->user_id, $ipAddress, $userAgent);
|
||||
user_login_attempt_record(true, $userData->getId(), $ipAddress, $userAgent);
|
||||
$sessionKey = user_session_create($userData->getId(), $ipAddress, $userAgent);
|
||||
|
||||
if(empty($sessionKey)) {
|
||||
$notices[] = "Something broke while creating a session for you, please tell an administrator or developer about this!";
|
||||
break;
|
||||
}
|
||||
|
||||
user_session_start($userData->user_id, $sessionKey);
|
||||
user_session_start($userData->getId(), $sessionKey);
|
||||
|
||||
$cookieLife = strtotime(user_session_current('session_expires'));
|
||||
$cookieValue = Base64::encode(user_session_cookie_pack($userData->user_id, $sessionKey), true);
|
||||
$cookieValue = Base64::encode(user_session_cookie_pack($userData->getId(), $sessionKey), true);
|
||||
setcookie('msz_auth', $cookieValue, $cookieLife, '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
|
||||
|
||||
if(!is_local_url($loginRedirect)) {
|
||||
|
|
|
@ -82,8 +82,8 @@ while(!$restricted && !empty($register)) {
|
|||
break;
|
||||
}
|
||||
|
||||
user_role_add($createUser->user_id, MSZ_ROLE_MAIN);
|
||||
url_redirect('auth-login-welcome', ['username' => $createUser->username]);
|
||||
user_role_add($createUser->getId(), MSZ_ROLE_MAIN);
|
||||
url_redirect('auth-login-welcome', ['username' => $createUser->getUsername()]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Comments\CommentsCategory;
|
||||
use Misuzu\Comments\CommentsCategoryNotFoundException;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserNotFoundException;
|
||||
|
||||
require_once '../misuzu.php';
|
||||
|
||||
$changelogChange = !empty($_GET['c']) && is_string($_GET['c']) ? (int)$_GET['c'] : 0;
|
||||
$changelogDate = !empty($_GET['d']) && is_string($_GET['d']) ? (string)$_GET['d'] : '';
|
||||
$changelogUser = !empty($_GET['u']) && is_string($_GET['u']) ? (int)$_GET['u'] : 0;
|
||||
$changelogTags = !empty($_GET['t']) && is_string($_GET['t']) ? (string)$_GET['t'] : '';
|
||||
|
||||
Template::set('comments_perms', $commentPerms = comments_get_perms(user_session_current('user_id', 0)));
|
||||
$changelogChange = !empty($_GET['c']) && is_string($_GET['c']) ? (int)$_GET['c'] : 0;
|
||||
$changelogDate = !empty($_GET['d']) && is_string($_GET['d']) ? (string)$_GET['d'] : '';
|
||||
$changelogUser = !empty($_GET['u']) && is_string($_GET['u']) ? (int)$_GET['u'] : 0;
|
||||
$changelogTags = !empty($_GET['t']) && is_string($_GET['t']) ? (string)$_GET['t'] : '';
|
||||
|
||||
if($changelogChange > 0) {
|
||||
$change = changelog_change_get($changelogChange);
|
||||
|
@ -18,14 +21,25 @@ if($changelogChange > 0) {
|
|||
return;
|
||||
}
|
||||
|
||||
$commentsCategoryName = "changelog-date-{$change['change_date']}";
|
||||
try {
|
||||
$commentsCategory = CommentsCategory::byName($commentsCategoryName);
|
||||
} catch(CommentsCategoryNotFoundException $ex) {
|
||||
$commentsCategory = new CommentsCategory($commentsCategoryName);
|
||||
$commentsCategory->save();
|
||||
}
|
||||
|
||||
try {
|
||||
$commentsUser = User::byId(user_session_current('user_id', 0));
|
||||
} catch(UserNotFoundException $ex) {
|
||||
$commentsUser = null;
|
||||
}
|
||||
|
||||
Template::render('changelog.change', [
|
||||
'change' => $change,
|
||||
'tags' => changelog_change_tags_get($change['change_id']),
|
||||
'comments_category' => $commentsCategory = comments_category_info(
|
||||
"changelog-date-{$change['change_date']}",
|
||||
true
|
||||
),
|
||||
'comments' => comments_category_get($commentsCategory['category_id'], user_session_current('user_id', 0)),
|
||||
'comments_category' => $commentsCategory,
|
||||
'comments_user' => $commentsUser,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
@ -52,9 +66,23 @@ if(!$changes) {
|
|||
}
|
||||
|
||||
if(!empty($changelogDate) && count($changes) > 0) {
|
||||
$commentsCategoryName = "changelog-date-{$changelogDate}";
|
||||
try {
|
||||
$commentsCategory = CommentsCategory::byName($commentsCategoryName);
|
||||
} catch(CommentsCategoryNotFoundException $ex) {
|
||||
$commentsCategory = new CommentsCategory($commentsCategoryName);
|
||||
$commentsCategory->save();
|
||||
}
|
||||
|
||||
try {
|
||||
$commentsUser = User::byId(user_session_current('user_id', 0));
|
||||
} catch(UserNotFoundException $ex) {
|
||||
$commentsUser = null;
|
||||
}
|
||||
|
||||
Template::set([
|
||||
'comments_category' => $commentsCategory = comments_category_info("changelog-date-{$changelogDate}", true),
|
||||
'comments' => comments_category_get($commentsCategory['category_id'], user_session_current('user_id', 0)),
|
||||
'comments_category' => $commentsCategory,
|
||||
'comments_user' => $commentsUser,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Comments\CommentsCategory;
|
||||
use Misuzu\Comments\CommentsCategoryNotFoundException;
|
||||
use Misuzu\Comments\CommentsPost;
|
||||
use Misuzu\Comments\CommentsPostNotFoundException;
|
||||
use Misuzu\Comments\CommentsPostSaveFailedException;
|
||||
use Misuzu\Comments\CommentsVote;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserNotFoundException;
|
||||
|
||||
require_once '../misuzu.php';
|
||||
|
||||
// basing whether or not this is an xhr request on whether a referrer header is present
|
||||
|
@ -20,28 +29,36 @@ if(!CSRF::validateRequest()) {
|
|||
return;
|
||||
}
|
||||
|
||||
if(!user_session_active()) {
|
||||
try {
|
||||
$currentUserInfo = User::byId(user_session_current('user_id', 0));
|
||||
} catch(UserNotFoundException $ex) {
|
||||
echo render_info_or_json($isXHR, 'You must be logged in to manage comments.', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = user_session_current('user_id', 0);
|
||||
|
||||
if(user_warning_check_expiration($currentUserId, MSZ_WARN_BAN) > 0) {
|
||||
if(user_warning_check_expiration($currentUserInfo->getId(), MSZ_WARN_BAN) > 0) {
|
||||
echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403);
|
||||
return;
|
||||
}
|
||||
if(user_warning_check_expiration($currentUserId, MSZ_WARN_SILENCE) > 0) {
|
||||
if(user_warning_check_expiration($currentUserInfo->getId(), MSZ_WARN_SILENCE) > 0) {
|
||||
echo render_info_or_json($isXHR, 'You have been silenced, check your profile for more information.', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
header(CSRF::header());
|
||||
$commentPerms = comments_get_perms($currentUserId);
|
||||
$commentPerms = $currentUserInfo->commentPerms();
|
||||
|
||||
$commentId = !empty($_GET['c']) && is_string($_GET['c']) ? (int)$_GET['c'] : 0;
|
||||
$commentMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
|
||||
$commentVote = !empty($_GET['v']) && is_string($_GET['v']) ? (int)$_GET['v'] : MSZ_COMMENTS_VOTE_INDIFFERENT;
|
||||
$commentId = (int)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
|
||||
$commentMode = filter_input(INPUT_GET, 'm');
|
||||
$commentVote = (int)filter_input(INPUT_GET, 'v', FILTER_SANITIZE_NUMBER_INT);
|
||||
|
||||
if($commentId > 0)
|
||||
try {
|
||||
$commentInfo2 = CommentsPost::byId($commentId);
|
||||
} catch(CommentsPostNotFoundException $ex) {
|
||||
echo render_info_or_json($isXHR, 'Post not found.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
switch($commentMode) {
|
||||
case 'pin':
|
||||
|
@ -51,38 +68,37 @@ switch($commentMode) {
|
|||
break;
|
||||
}
|
||||
|
||||
$commentInfo = comments_post_get($commentId, false);
|
||||
|
||||
if(!$commentInfo || $commentInfo['comment_deleted'] !== null) {
|
||||
if($commentInfo2->isDeleted()) {
|
||||
echo render_info_or_json($isXHR, "This comment doesn't exist!", 400);
|
||||
break;
|
||||
}
|
||||
|
||||
if($commentInfo['comment_reply_to'] !== null) {
|
||||
if($commentInfo2->hasParent()) {
|
||||
echo render_info_or_json($isXHR, "You can't pin replies!", 400);
|
||||
break;
|
||||
}
|
||||
|
||||
$isPinning = $commentMode === 'pin';
|
||||
|
||||
if($isPinning && !empty($commentInfo['comment_pinned'])) {
|
||||
if($isPinning && $commentInfo2->isPinned()) {
|
||||
echo render_info_or_json($isXHR, 'This comment is already pinned.', 400);
|
||||
break;
|
||||
} elseif(!$isPinning && empty($commentInfo['comment_pinned'])) {
|
||||
} elseif(!$isPinning && !$commentInfo2->isPinned()) {
|
||||
echo render_info_or_json($isXHR, "This comment isn't pinned yet.", 400);
|
||||
break;
|
||||
}
|
||||
|
||||
$commentPinned = comments_pin_status($commentInfo['comment_id'], $isPinning);
|
||||
$commentInfo2->setPinned($isPinning);
|
||||
$commentInfo2->save();
|
||||
|
||||
if(!$isXHR) {
|
||||
redirect($redirect . '#comment-' . $commentInfo['comment_id']);
|
||||
redirect($redirect . '#comment-' . $commentInfo2->getId());
|
||||
break;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'comment_id' => $commentInfo['comment_id'],
|
||||
'comment_pinned' => $commentPinned,
|
||||
'comment_id' => $commentInfo2->getId(),
|
||||
'comment_pinned' => ($time = $commentInfo2->getPinnedTime()) < 0 ? null : date('Y-m-d H:i:s', $time),
|
||||
]);
|
||||
break;
|
||||
|
||||
|
@ -92,30 +108,24 @@ switch($commentMode) {
|
|||
break;
|
||||
}
|
||||
|
||||
if(!comments_vote_type_valid($commentVote)) {
|
||||
echo render_info_or_json($isXHR, 'Invalid vote action.', 400);
|
||||
break;
|
||||
}
|
||||
|
||||
$commentInfo = comments_post_get($commentId, false);
|
||||
|
||||
if(!$commentInfo || $commentInfo['comment_deleted'] !== null) {
|
||||
if($commentInfo2->isDeleted()) {
|
||||
echo render_info_or_json($isXHR, "This comment doesn't exist!", 400);
|
||||
break;
|
||||
}
|
||||
|
||||
$voteResult = comments_vote_add(
|
||||
$commentInfo['comment_id'],
|
||||
user_session_current('user_id', 0),
|
||||
$commentVote
|
||||
);
|
||||
if($commentVote > 0)
|
||||
$commentInfo2->addPositiveVote($currentUserInfo);
|
||||
elseif($commentVote < 0)
|
||||
$commentInfo2->addNegativeVote($currentUserInfo);
|
||||
else
|
||||
$commentInfo2->removeVote($currentUserInfo);
|
||||
|
||||
if(!$isXHR) {
|
||||
redirect($redirect . '#comment-' . $commentInfo['comment_id']);
|
||||
redirect($redirect . '#comment-' . $commentInfo2->getId());
|
||||
break;
|
||||
}
|
||||
|
||||
echo json_encode(comments_votes_get($commentInfo['comment_id']));
|
||||
echo json_encode($commentInfo2->votes());
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
|
@ -124,17 +134,7 @@ switch($commentMode) {
|
|||
break;
|
||||
}
|
||||
|
||||
$commentInfo = comments_post_get($commentId, false);
|
||||
|
||||
if(!$commentInfo) {
|
||||
echo render_info_or_json($isXHR, "This comment doesn't exist.", 400);
|
||||
break;
|
||||
}
|
||||
|
||||
$isOwnComment = (int)$commentInfo['user_id'] === $currentUserId;
|
||||
$isModAction = $commentPerms['can_delete_any'] && !$isOwnComment;
|
||||
|
||||
if($commentInfo['comment_deleted'] !== null) {
|
||||
if($commentInfo2->isDeleted()) {
|
||||
echo render_info_or_json(
|
||||
$isXHR,
|
||||
$commentPerms['can_delete_any'] ? 'This comment is already marked for deletion.' : "This comment doesn't exist.",
|
||||
|
@ -143,24 +143,25 @@ switch($commentMode) {
|
|||
break;
|
||||
}
|
||||
|
||||
$isOwnComment = $commentInfo2->getUserId() === $currentUserInfo->getId();
|
||||
$isModAction = $commentPerms['can_delete_any'] && !$isOwnComment;
|
||||
|
||||
if(!$isModAction && !$isOwnComment) {
|
||||
echo render_info_or_json($isXHR, "You're not allowed to delete comments made by others.", 403);
|
||||
break;
|
||||
}
|
||||
|
||||
if(!comments_post_delete($commentInfo['comment_id'])) {
|
||||
echo render_info_or_json($isXHR, 'Failed to delete comment.', 500);
|
||||
break;
|
||||
}
|
||||
$commentInfo2->setDeleted(true);
|
||||
$commentInfo2->save();
|
||||
|
||||
if($isModAction) {
|
||||
audit_log(MSZ_AUDIT_COMMENT_ENTRY_DELETE_MOD, $currentUserId, [
|
||||
$commentInfo['comment_id'],
|
||||
(int)($commentInfo['user_id'] ?? 0),
|
||||
$commentInfo['username'] ?? '(Deleted User)',
|
||||
audit_log(MSZ_AUDIT_COMMENT_ENTRY_DELETE_MOD, $currentUserInfo->getId(), [
|
||||
$commentInfo2->getId(),
|
||||
$commentUserId = $commentInfo2->getUserId(),
|
||||
($commentUserId < 1 ? '(Deleted User)' : $commentInfo2->getUser()->getUsername()),
|
||||
]);
|
||||
} else {
|
||||
audit_log(MSZ_AUDIT_COMMENT_ENTRY_DELETE, $currentUserId, [$commentInfo['comment_id']]);
|
||||
audit_log(MSZ_AUDIT_COMMENT_ENTRY_DELETE, $currentUserInfo->getId(), [$commentInfo2->getId()]);
|
||||
}
|
||||
|
||||
if($redirect) {
|
||||
|
@ -169,7 +170,7 @@ switch($commentMode) {
|
|||
}
|
||||
|
||||
echo json_encode([
|
||||
'id' => $commentInfo['comment_id'],
|
||||
'id' => $commentInfo2->getId(),
|
||||
]);
|
||||
break;
|
||||
|
||||
|
@ -179,36 +180,27 @@ switch($commentMode) {
|
|||
break;
|
||||
}
|
||||
|
||||
$commentInfo = comments_post_get($commentId, false);
|
||||
|
||||
if(!$commentInfo) {
|
||||
echo render_info_or_json($isXHR, "This comment doesn't exist.", 400);
|
||||
break;
|
||||
}
|
||||
|
||||
if($commentInfo['comment_deleted'] === null) {
|
||||
if(!$commentInfo2->isDeleted()) {
|
||||
echo render_info_or_json($isXHR, "This comment isn't in a deleted state.", 400);
|
||||
break;
|
||||
}
|
||||
|
||||
if(!comments_post_delete($commentInfo['comment_id'], false)) {
|
||||
echo render_info_or_json($isXHR, 'Failed to restore comment.', 500);
|
||||
break;
|
||||
}
|
||||
$commentInfo2->setDeleted(false);
|
||||
$commentInfo2->save();
|
||||
|
||||
audit_log(MSZ_AUDIT_COMMENT_ENTRY_RESTORE, $currentUserId, [
|
||||
$commentInfo['comment_id'],
|
||||
(int)($commentInfo['user_id'] ?? 0),
|
||||
$commentInfo['username'] ?? '(Deleted User)',
|
||||
audit_log(MSZ_AUDIT_COMMENT_ENTRY_RESTORE, $currentUserInfo->getId(), [
|
||||
$commentInfo2->getId(),
|
||||
$commentUserId = $commentInfo2->getUserId(),
|
||||
($commentUserId < 1 ? '(Deleted User)' : $commentInfo2->getUser()->getUsername()),
|
||||
]);
|
||||
|
||||
if($redirect) {
|
||||
redirect($redirect . '#comment-' . $commentInfo['comment_id']);
|
||||
redirect($redirect . '#comment-' . $commentInfo2->getId());
|
||||
break;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'id' => $commentInfo['comment_id'],
|
||||
'id' => $commentInfo2->getId(),
|
||||
]);
|
||||
break;
|
||||
|
||||
|
@ -223,26 +215,30 @@ switch($commentMode) {
|
|||
break;
|
||||
}
|
||||
|
||||
$categoryId = !empty($_POST['comment']['category']) && is_string($_POST['comment']['category']) ? (int)$_POST['comment']['category'] : 0;
|
||||
$category = comments_category_info($categoryId);
|
||||
|
||||
if(!$category) {
|
||||
try {
|
||||
$categoryInfo = CommentsCategory::byId(
|
||||
isset($_POST['comment']['category']) && is_string($_POST['comment']['category'])
|
||||
? (int)$_POST['comment']['category']
|
||||
: 0
|
||||
);
|
||||
} catch(CommentsCategoryNotFoundException $ex) {
|
||||
echo render_info_or_json($isXHR, 'This comment category doesn\'t exist.', 404);
|
||||
break;
|
||||
}
|
||||
|
||||
if(!is_null($category['category_locked']) && !$commentPerms['can_lock']) {
|
||||
if($categoryInfo->isLocked() && !$commentPerms['can_lock']) {
|
||||
echo render_info_or_json($isXHR, 'This comment category has been locked.', 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$commentText = !empty($_POST['comment']['text']) && is_string($_POST['comment']['text']) ? $_POST['comment']['text'] : '';
|
||||
$commentLock = !empty($_POST['comment']['lock']) && $commentPerms['can_lock'];
|
||||
$commentPin = !empty($_POST['comment']['pin']) && $commentPerms['can_pin'];
|
||||
$commentText = !empty($_POST['comment']['text']) && is_string($_POST['comment']['text']) ? $_POST['comment']['text'] : '';
|
||||
$commentReply = !empty($_POST['comment']['reply']) && is_string($_POST['comment']['reply']) ? (int)$_POST['comment']['reply'] : 0;
|
||||
$commentLock = !empty($_POST['comment']['lock']) && $commentPerms['can_lock'];
|
||||
$commentPin = !empty($_POST['comment']['pin']) && $commentPerms['can_pin'];
|
||||
|
||||
if($commentLock) {
|
||||
comments_category_lock($categoryId, is_null($category['category_locked']));
|
||||
$categoryInfo->setLocked(!$categoryInfo->isLocked());
|
||||
$categoryInfo->save();
|
||||
}
|
||||
|
||||
if(strlen($commentText) > 0) {
|
||||
|
@ -261,30 +257,53 @@ switch($commentMode) {
|
|||
break;
|
||||
}
|
||||
|
||||
if($commentReply > 0 && !comments_post_exists($commentReply)) {
|
||||
echo render_info_or_json($isXHR, 'The comment you tried to reply to does not exist.', 404);
|
||||
break;
|
||||
if($commentReply > 0) {
|
||||
try {
|
||||
$parentCommentInfo = CommentsPost::byId($commentReply);
|
||||
} catch(CommentsPostNotFoundException $ex) {
|
||||
unset($parentCommentInfo);
|
||||
}
|
||||
|
||||
if(!isset($parentCommentInfo) || $parentCommentInfo->isDeleted()) {
|
||||
echo render_info_or_json($isXHR, 'The comment you tried to reply to does not exist.', 404);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$commentId = comments_post_create(
|
||||
user_session_current('user_id', 0),
|
||||
$categoryId,
|
||||
$commentText,
|
||||
$commentPin,
|
||||
$commentReply
|
||||
);
|
||||
$commentInfo2 = (new CommentsPost)
|
||||
->setUser($currentUserInfo)
|
||||
->setCategory($categoryInfo)
|
||||
->setParsedText($commentText)
|
||||
->setPinned($commentPin);
|
||||
|
||||
if($commentId < 1) {
|
||||
if(isset($parentCommentInfo))
|
||||
$commentInfo2->setParent($parentCommentInfo);
|
||||
|
||||
try {
|
||||
$commentInfo2->save();
|
||||
} catch(CommentsPostSaveFailedException $ex) {
|
||||
echo render_info_or_json($isXHR, 'Something went horribly wrong.', 500);
|
||||
break;
|
||||
}
|
||||
|
||||
if($redirect) {
|
||||
redirect($redirect . '#comment-' . $commentId);
|
||||
redirect($redirect . '#comment-' . $commentInfo2->getId());
|
||||
break;
|
||||
}
|
||||
|
||||
echo json_encode(comments_post_get($commentId));
|
||||
echo json_encode([
|
||||
'comment_id' => $commentInfo2->getId(),
|
||||
'category_id' => $commentInfo2->getCategoryId(),
|
||||
'comment_text' => $commentInfo2->getText(),
|
||||
'comment_created' => ($time = $commentInfo2->getCreatedTime()) < 0 ? null : date('Y-m-d H:i:s', $time),
|
||||
'comment_edited' => ($time = $commentInfo2->getEditedTime()) < 0 ? null : date('Y-m-d H:i:s', $time),
|
||||
'comment_deleted' => ($time = $commentInfo2->getDeletedTime()) < 0 ? null : date('Y-m-d H:i:s', $time),
|
||||
'comment_pinned' => ($time = $commentInfo2->getPinnedTime()) < 0 ? null : date('Y-m-d H:i:s', $time),
|
||||
'comment_reply_to' => ($parent = $commentInfo2->getParentId()) < 1 ? null : $parent,
|
||||
'user_id' => ($commentInfo2->getUserId() < 1 ? null : $commentInfo2->getUser()->getId()),
|
||||
'username' => ($commentInfo2->getUserId() < 1 ? null : $commentInfo2->getUser()->getUsername()),
|
||||
'user_colour' => ($commentInfo2->getUserId() < 1 ? 0x40000000 : $commentInfo2->getUser()->getColour()->getRaw()),
|
||||
]);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
|
@ -72,9 +72,13 @@ $responseStatus = $response->getStatusCode();
|
|||
|
||||
header('HTTP/' . $response->getProtocolVersion() . ' ' . $responseStatus . ' ' . $response->getReasonPhrase());
|
||||
|
||||
foreach($response->getHeaders() as $headerName => $headerSet)
|
||||
foreach($headerSet as $headerLine)
|
||||
header("{$headerName}: {$headerLine}");
|
||||
foreach($response->getHeaders() as $name => $lines) {
|
||||
$firstLine = true;
|
||||
foreach($lines as $line) {
|
||||
header("{$name}: {$line}", $firstLine);
|
||||
$firstLine = false;
|
||||
}
|
||||
}
|
||||
|
||||
$responseBody = $response->getBody();
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ namespace Misuzu;
|
|||
|
||||
use Misuzu\Parsers\Parser;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserNotFoundException;
|
||||
|
||||
require_once '../misuzu.php';
|
||||
|
||||
|
@ -10,9 +11,9 @@ $userId = !empty($_GET['u']) && is_string($_GET['u']) ? $_GET['u'] : 0;
|
|||
$profileMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
|
||||
$isEditing = !empty($_GET['edit']) && is_string($_GET['edit']) ? (bool)$_GET['edit'] : !empty($_POST) && is_array($_POST);
|
||||
|
||||
$profileUser = User::findForProfile($userId);
|
||||
|
||||
if(empty($profileUser)) {
|
||||
try {
|
||||
$profileUser = User::findForProfile($userId);
|
||||
} catch(UserNotFoundException $ex) {
|
||||
http_response_code(404);
|
||||
Template::render('profile.index');
|
||||
return;
|
||||
|
@ -22,9 +23,9 @@ $notices = [];
|
|||
|
||||
$currentUserId = user_session_current('user_id', 0);
|
||||
$viewingAsGuest = $currentUserId === 0;
|
||||
$viewingOwnProfile = $currentUserId === $profileUser->user_id;
|
||||
$viewingOwnProfile = $currentUserId === $profileUser->getId();
|
||||
|
||||
$isBanned = user_warning_check_restriction($profileUser->user_id);
|
||||
$isBanned = user_warning_check_restriction($profileUser->getId());
|
||||
$userPerms = perms_get_user($currentUserId)[MSZ_PERMS_USER];
|
||||
$canManageWarnings = perms_check($userPerms, MSZ_PERM_USER_MANAGE_WARNINGS);
|
||||
$canEdit = !$isBanned
|
||||
|
@ -34,7 +35,7 @@ $canEdit = !$isBanned
|
|||
|| user_check_super($currentUserId)
|
||||
|| (
|
||||
perms_check($userPerms, MSZ_PERM_USER_MANAGE_USERS)
|
||||
&& user_check_authority($currentUserId, $profileUser->user_id)
|
||||
&& user_check_authority($currentUserId, $profileUser->getId())
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -87,7 +88,7 @@ if($isEditing) {
|
|||
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['about']['not-allowed'];
|
||||
} else {
|
||||
$setAboutError = user_set_about_page(
|
||||
$profileUser->user_id,
|
||||
$profileUser->getId(),
|
||||
$_POST['about']['text'] ?? '',
|
||||
(int)($_POST['about']['parser'] ?? Parser::PLAIN)
|
||||
);
|
||||
|
@ -106,7 +107,7 @@ if($isEditing) {
|
|||
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['signature']['not-allowed'];
|
||||
} else {
|
||||
$setSignatureError = user_set_signature(
|
||||
$profileUser->user_id,
|
||||
$profileUser->getId(),
|
||||
$_POST['signature']['text'] ?? '',
|
||||
(int)($_POST['signature']['parser'] ?? Parser::PLAIN)
|
||||
);
|
||||
|
@ -125,7 +126,7 @@ if($isEditing) {
|
|||
$notices[] = "You aren't allow to change your birthdate.";
|
||||
} else {
|
||||
$setBirthdate = user_set_birthdate(
|
||||
$profileUser->user_id,
|
||||
$profileUser->getId(),
|
||||
(int)($_POST['birthdate']['day'] ?? 0),
|
||||
(int)($_POST['birthdate']['month'] ?? 0),
|
||||
(int)($_POST['birthdate']['year'] ?? 0)
|
||||
|
@ -154,7 +155,7 @@ if($isEditing) {
|
|||
|
||||
if(!empty($_FILES['avatar'])) {
|
||||
if(!empty($_POST['avatar']['delete'])) {
|
||||
user_avatar_delete($profileUser->user_id);
|
||||
user_avatar_delete($profileUser->getId());
|
||||
} else {
|
||||
if(!$perms['edit_avatar']) {
|
||||
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['avatar']['not-allowed'];
|
||||
|
@ -172,7 +173,7 @@ if($isEditing) {
|
|||
);
|
||||
} else {
|
||||
$setAvatar = user_avatar_set_from_path(
|
||||
$profileUser->user_id,
|
||||
$profileUser->getId(),
|
||||
$_FILES['avatar']['tmp_name']['file'],
|
||||
$avatarProps
|
||||
);
|
||||
|
@ -194,8 +195,8 @@ if($isEditing) {
|
|||
|
||||
if(!empty($_FILES['background'])) {
|
||||
if((int)($_POST['background']['attach'] ?? -1) === 0) {
|
||||
user_background_delete($profileUser->user_id);
|
||||
user_background_set_settings($profileUser->user_id, MSZ_USER_BACKGROUND_ATTACHMENT_NONE);
|
||||
user_background_delete($profileUser->getId());
|
||||
user_background_set_settings($profileUser->getId(), MSZ_USER_BACKGROUND_ATTACHMENT_NONE);
|
||||
} else {
|
||||
if(!$perms['edit_background']) {
|
||||
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['background']['not-allowed'];
|
||||
|
@ -213,7 +214,7 @@ if($isEditing) {
|
|||
);
|
||||
} else {
|
||||
$setBackground = user_background_set_from_path(
|
||||
$profileUser->user_id,
|
||||
$profileUser->getId(),
|
||||
$_FILES['background']['tmp_name']['file'],
|
||||
$backgroundProps
|
||||
);
|
||||
|
@ -243,7 +244,7 @@ if($isEditing) {
|
|||
$backgroundSettings |= MSZ_USER_BACKGROUND_ATTRIBUTE_SLIDE;
|
||||
}
|
||||
|
||||
user_background_set_settings($profileUser->user_id, $backgroundSettings);
|
||||
user_background_set_settings($profileUser->getId(), $backgroundSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -294,23 +295,23 @@ $profileStats = DB::prepare(sprintf('
|
|||
) AS `following_count`
|
||||
FROM `msz_users` AS u
|
||||
WHERE `user_id` = :user_id
|
||||
', MSZ_USER_RELATION_FOLLOW))->bind('user_id', $profileUser->user_id)->fetch();
|
||||
', MSZ_USER_RELATION_FOLLOW))->bind('user_id', $profileUser->getId())->fetch();
|
||||
|
||||
$relationInfo = user_session_active()
|
||||
? user_relation_info($currentUserId, $profileUser->user_id)
|
||||
? user_relation_info($currentUserId, $profileUser->getId())
|
||||
: [];
|
||||
|
||||
$backgroundPath = sprintf('%s/backgrounds/original/%d.msz', MSZ_STORAGE, $profileUser->user_id);
|
||||
$backgroundPath = sprintf('%s/backgrounds/original/%d.msz', MSZ_STORAGE, $profileUser->getId());
|
||||
|
||||
if(is_file($backgroundPath)) {
|
||||
$backgroundInfo = getimagesize($backgroundPath);
|
||||
|
||||
if($backgroundInfo) {
|
||||
Template::set('site_background', [
|
||||
'url' => url('user-background', ['user' => $profileUser->user_id]),
|
||||
'url' => url('user-background', ['user' => $profileUser->getId()]),
|
||||
'width' => $backgroundInfo[0],
|
||||
'height' => $backgroundInfo[1],
|
||||
'settings' => $profileUser->user_background_settings,
|
||||
'settings' => $profileUser->getBackgroundSettings(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -322,7 +323,7 @@ switch($profileMode) {
|
|||
|
||||
case 'following':
|
||||
$template = 'profile.relations';
|
||||
$followingCount = user_relation_count_from($profileUser->user_id, MSZ_USER_RELATION_FOLLOW);
|
||||
$followingCount = user_relation_count_from($profileUser->getId(), MSZ_USER_RELATION_FOLLOW);
|
||||
$followingPagination = new Pagination($followingCount, MSZ_USER_RELATION_FOLLOW_PER_PAGE);
|
||||
|
||||
if(!$followingPagination->hasValidOffset()) {
|
||||
|
@ -331,14 +332,14 @@ switch($profileMode) {
|
|||
}
|
||||
|
||||
$following = user_relation_users_from(
|
||||
$profileUser->user_id, MSZ_USER_RELATION_FOLLOW,
|
||||
$profileUser->getId(), MSZ_USER_RELATION_FOLLOW,
|
||||
$followingPagination->getRange(), $followingPagination->getOffset(),
|
||||
$currentUserId
|
||||
);
|
||||
|
||||
Template::set([
|
||||
'title' => $profileUser->username . ' / following',
|
||||
'canonical_url' => url('user-profile-following', ['user' => $profileUser->user_id]),
|
||||
'title' => $profileUser->getUsername() . ' / following',
|
||||
'canonical_url' => url('user-profile-following', ['user' => $profileUser->getId()]),
|
||||
'profile_users' => $following,
|
||||
'profile_relation_pagination' => $followingPagination,
|
||||
]);
|
||||
|
@ -346,7 +347,7 @@ switch($profileMode) {
|
|||
|
||||
case 'followers':
|
||||
$template = 'profile.relations';
|
||||
$followerCount = user_relation_count_to($profileUser->user_id, MSZ_USER_RELATION_FOLLOW);
|
||||
$followerCount = user_relation_count_to($profileUser->getId(), MSZ_USER_RELATION_FOLLOW);
|
||||
$followerPagination = new Pagination($followerCount, MSZ_USER_RELATION_FOLLOW_PER_PAGE);
|
||||
|
||||
if(!$followerPagination->hasValidOffset()) {
|
||||
|
@ -355,14 +356,14 @@ switch($profileMode) {
|
|||
}
|
||||
|
||||
$followers = user_relation_users_to(
|
||||
$profileUser->user_id, MSZ_USER_RELATION_FOLLOW,
|
||||
$profileUser->getId(), MSZ_USER_RELATION_FOLLOW,
|
||||
$followerPagination->getRange(), $followerPagination->getOffset(),
|
||||
$currentUserId
|
||||
);
|
||||
|
||||
Template::set([
|
||||
'title' => $profileUser->username . ' / followers',
|
||||
'canonical_url' => url('user-profile-followers', ['user' => $profileUser->user_id]),
|
||||
'title' => $profileUser->getUsername() . ' / followers',
|
||||
'canonical_url' => url('user-profile-followers', ['user' => $profileUser->getId()]),
|
||||
'profile_users' => $followers,
|
||||
'profile_relation_pagination' => $followerPagination,
|
||||
]);
|
||||
|
@ -370,7 +371,7 @@ switch($profileMode) {
|
|||
|
||||
case 'forum-topics':
|
||||
$template = 'profile.topics';
|
||||
$topicsCount = forum_topic_count_user($profileUser->user_id, $currentUserId);
|
||||
$topicsCount = forum_topic_count_user($profileUser->getId(), $currentUserId);
|
||||
$topicsPagination = new Pagination($topicsCount, 20);
|
||||
|
||||
if(!$topicsPagination->hasValidOffset()) {
|
||||
|
@ -379,13 +380,13 @@ switch($profileMode) {
|
|||
}
|
||||
|
||||
$topics = forum_topic_listing_user(
|
||||
$profileUser->user_id, $currentUserId,
|
||||
$profileUser->getId(), $currentUserId,
|
||||
$topicsPagination->getOffset(), $topicsPagination->getRange()
|
||||
);
|
||||
|
||||
Template::set([
|
||||
'title' => $profileUser->username . ' / topics',
|
||||
'canonical_url' => url('user-profile-forum-topics', ['user' => $profileUser->user_id, 'page' => Pagination::param()]),
|
||||
'title' => $profileUser->getUsername() . ' / topics',
|
||||
'canonical_url' => url('user-profile-forum-topics', ['user' => $profileUser->getId(), 'page' => Pagination::param()]),
|
||||
'profile_topics' => $topics,
|
||||
'profile_topics_pagination' => $topicsPagination,
|
||||
]);
|
||||
|
@ -393,7 +394,7 @@ switch($profileMode) {
|
|||
|
||||
case 'forum-posts':
|
||||
$template = 'profile.posts';
|
||||
$postsCount = forum_post_count_user($profileUser->user_id);
|
||||
$postsCount = forum_post_count_user($profileUser->getId());
|
||||
$postsPagination = new Pagination($postsCount, 20);
|
||||
|
||||
if(!$postsPagination->hasValidOffset()) {
|
||||
|
@ -402,7 +403,7 @@ switch($profileMode) {
|
|||
}
|
||||
|
||||
$posts = forum_post_listing(
|
||||
$profileUser->user_id,
|
||||
$profileUser->getId(),
|
||||
$postsPagination->getOffset(),
|
||||
$postsPagination->getRange(),
|
||||
false,
|
||||
|
@ -410,8 +411,8 @@ switch($profileMode) {
|
|||
);
|
||||
|
||||
Template::set([
|
||||
'title' => $profileUser->username . ' / posts',
|
||||
'canonical_url' => url('user-profile-forum-posts', ['user' => $profileUser->user_id, 'page' => Pagination::param()]),
|
||||
'title' => $profileUser->getUsername() . ' / posts',
|
||||
'canonical_url' => url('user-profile-forum-posts', ['user' => $profileUser->getId(), 'page' => Pagination::param()]),
|
||||
'profile_posts' => $posts,
|
||||
'profile_posts_pagination' => $postsPagination,
|
||||
]);
|
||||
|
@ -422,7 +423,7 @@ switch($profileMode) {
|
|||
$warnings = $viewingAsGuest
|
||||
? []
|
||||
: user_warning_fetch(
|
||||
$profileUser->user_id,
|
||||
$profileUser->getId(),
|
||||
90,
|
||||
$canManageWarnings
|
||||
? MSZ_WARN_TYPES_VISIBLE_TO_STAFF
|
||||
|
|
|
@ -14,7 +14,7 @@ if(!user_session_active()) {
|
|||
|
||||
$errors = [];
|
||||
$currentUserId = user_session_current('user_id');
|
||||
$currentUser = User::get($currentUserId);
|
||||
$currentUser = User::byId($currentUserId);
|
||||
$currentEmail = user_email_get($currentUserId);
|
||||
$isRestricted = user_warning_check_restriction($currentUserId);
|
||||
$twoFactorInfo = user_totp_info($currentUserId);
|
||||
|
|
|
@ -27,7 +27,7 @@ function db_to_zip(ZipArchive $archive, int $userId, string $filename, string $q
|
|||
|
||||
$errors = [];
|
||||
$currentUserId = user_session_current('user_id');
|
||||
$currentUser = User::get($currentUserId);
|
||||
$currentUser = User::byId($currentUserId);
|
||||
|
||||
if(isset($_POST['action']) && is_string($_POST['action'])) {
|
||||
if(isset($_POST['password']) && is_string($_POST['password'])
|
||||
|
|
|
@ -3,15 +3,20 @@ namespace Misuzu;
|
|||
|
||||
use Misuzu\Imaging\Image;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserNotFoundException;
|
||||
|
||||
$userAssetsMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
|
||||
$misuzuBypassLockdown = $userAssetsMode === 'avatar';
|
||||
|
||||
require_once '../misuzu.php';
|
||||
|
||||
$userInfo = User::get((int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT));
|
||||
$userExists = empty($userExists);
|
||||
$userId = $userExists ? $userInfo->getUserId() : 0;
|
||||
try {
|
||||
$userInfo = User::byId((int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT));
|
||||
$userExists = true;
|
||||
} catch(UserNotFoundException $ex) {
|
||||
$userExists = false;
|
||||
}
|
||||
$userId = $userExists ? $userInfo->getId() : 0;
|
||||
|
||||
$canViewImages = !$userExists
|
||||
|| !user_warning_check_expiration($userId, MSZ_WARN_BAN)
|
||||
|
|
160
src/Comments/CommentsCategory.php
Normal file
160
src/Comments/CommentsCategory.php
Normal file
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
namespace Misuzu\Comments;
|
||||
|
||||
use JsonSerializable;
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Memoizer;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class CommentsCategoryException extends CommentsException {};
|
||||
class CommentsCategoryNotFoundException extends CommentsCategoryException {};
|
||||
|
||||
class CommentsCategory implements JsonSerializable {
|
||||
// Database fields
|
||||
private $category_id = -1;
|
||||
private $category_name = '';
|
||||
private $category_created = null;
|
||||
private $category_locked = null;
|
||||
|
||||
private $postCount = -1;
|
||||
|
||||
public const TABLE = 'comments_categories';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`category_id`, %1$s.`category_name`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`category_created`) AS `category_created`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`category_locked`) AS `category_locked`';
|
||||
|
||||
public function __construct(?string $name = null) {
|
||||
if($name !== null)
|
||||
$this->setName($name);
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->category_id < 1 ? -1 : $this->category_id;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->category_name;
|
||||
}
|
||||
public function setName(string $name): self {
|
||||
$this->category_name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedTime(): int {
|
||||
return $this->category_created === null ? -1 : $this->category_created;
|
||||
}
|
||||
|
||||
public function getLockedTime(): int {
|
||||
return $this->category_locked === null ? -1 : $this->category_locked;
|
||||
}
|
||||
public function isLocked(): bool {
|
||||
return $this->getLockedTime() >= 0;
|
||||
}
|
||||
public function setLocked(bool $locked): self {
|
||||
if($locked !== $this->isLocked())
|
||||
$this->category_locked = $locked ? time() : null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Purely cosmetic, do not use for anything other than displaying
|
||||
public function getPostCount(): int {
|
||||
if($this->postCount < 0)
|
||||
$this->postCount = (int)DB::prepare('
|
||||
SELECT COUNT(`comment_id`)
|
||||
FROM `msz_comments_posts`
|
||||
WHERE `category_id` = :cat_id
|
||||
AND `comment_deleted` IS NULL
|
||||
')->bind('cat_id', $this->getId())->fetchColumn();
|
||||
|
||||
return $this->postCount;
|
||||
}
|
||||
|
||||
public function jsonSerialize() {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'name' => $this->getName(),
|
||||
'created' => ($created = $this->getCreatedTime()) < 0 ? null : date('c', $created),
|
||||
'locked' => ($locked = $this->getLockedTime()) < 0 ? null : date('c', $locked),
|
||||
];
|
||||
}
|
||||
|
||||
public function save(): void {
|
||||
$isInsert = $this->getId() < 1;
|
||||
if($isInsert) {
|
||||
$query = 'INSERT INTO `%1$s%2$s` (`category_name`, `category_locked`) VALUES'
|
||||
. ' (:name, :locked)';
|
||||
} else {
|
||||
$query = 'UPDATE `%1$s%2$s` SET `category_name` = :name, `category_locked` = FROM_UNIXTIME(:locked)'
|
||||
. ' WHERE `category_id` = :category';
|
||||
}
|
||||
|
||||
$saveCategory = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
|
||||
->bind('name', $this->category_name)
|
||||
->bind('locked', $this->category_locked);
|
||||
|
||||
if($isInsert) {
|
||||
$this->category_id = $saveCategory->executeGetId();
|
||||
$this->category_created = time();
|
||||
} else {
|
||||
$saveCategory->bind('category', $this->getId())
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
public function posts(?User $voteUser = null, bool $includeVotes = true, ?Pagination $pagination = null, bool $rootOnly = true, bool $includeDeleted = true): array {
|
||||
return CommentsPost::byCategory($this, $voteUser, $includeVotes, $pagination, $rootOnly, $includeDeleted);
|
||||
}
|
||||
public function votes(?User $user = null, bool $rootOnly = true, ?Pagination $pagination = null): array {
|
||||
return CommentsVote::byCategory($this, $user, $rootOnly, $pagination);
|
||||
}
|
||||
|
||||
private static function getMemoizer() {
|
||||
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 {
|
||||
return self::getMemoizer()->find($categoryId, function() use ($categoryId) {
|
||||
$cat = DB::prepare(self::byQueryBase() . ' WHERE `category_id` = :cat_id')
|
||||
->bind('cat_id', $categoryId)
|
||||
->fetchObject(self::class);
|
||||
if(!$cat)
|
||||
throw new CommentsCategoryNotFoundException;
|
||||
return $cat;
|
||||
});
|
||||
}
|
||||
public static function byName(string $categoryName): self {
|
||||
return self::getMemoizer()->find(function($category) use ($categoryName) {
|
||||
return $category->getName() === $categoryName;
|
||||
}, function() use ($categoryName) {
|
||||
$cat = DB::prepare(self::byQueryBase() . ' WHERE `category_name` = :name')
|
||||
->bind('name', $categoryName)
|
||||
->fetchObject(self::class);
|
||||
if(!$cat)
|
||||
throw new CommentsCategoryNotFoundException;
|
||||
return $cat;
|
||||
});
|
||||
}
|
||||
public static function all(?Pagination $pagination = null): array {
|
||||
$catsQuery = self::byQueryBase()
|
||||
. ' ORDER BY `category_id` ASC';
|
||||
|
||||
if($pagination !== null)
|
||||
$catsQuery .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getCats = DB::prepare($catsQuery);
|
||||
|
||||
if($pagination !== null)
|
||||
$getCats->bind('range', $pagination->getRange())
|
||||
->bind('offset', $pagination->getOffset());
|
||||
|
||||
return $getCats->fetchObjects(self::class);
|
||||
}
|
||||
}
|
6
src/Comments/CommentsException.php
Normal file
6
src/Comments/CommentsException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace Misuzu\Comments;
|
||||
|
||||
use Exception;
|
||||
|
||||
class CommentsException extends Exception {}
|
59
src/Comments/CommentsParser.php
Normal file
59
src/Comments/CommentsParser.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
namespace Misuzu\Comments;
|
||||
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class CommentsParser {
|
||||
private const MARKUP_USERNAME = '#\B(?:@{1}(' . MSZ_USERNAME_REGEX . '))#u';
|
||||
private const MARKUP_USERID = '#\B(?:@{2}([0-9]+))#u';
|
||||
|
||||
public static function parseForStorage(string $text): string {
|
||||
return preg_replace_callback(self::MARKUP_USERNAME, function ($matches) {
|
||||
return ($userId = user_id_from_username($matches[1])) < 1
|
||||
? $matches[0] : "@@{$userId}";
|
||||
}, $text);
|
||||
}
|
||||
|
||||
public static function parseForDisplay(string $text): string {
|
||||
$text = htmlentities($text);
|
||||
|
||||
$text = preg_replace_callback(
|
||||
'/(^|[\n ])([\w]*?)([\w]*?:\/\/[\w]+[^ \,\"\n\r\t<]*)/is',
|
||||
function ($matches) {
|
||||
$matches[0] = trim($matches[0]);
|
||||
$url = parse_url($matches[0]);
|
||||
if(empty($url['scheme']) || !in_array(mb_strtolower($url['scheme']), ['http', 'https'], true))
|
||||
return $matches[0];
|
||||
return sprintf(' <a href="%1$s" class="link" target="_blank" rel="noreferrer noopener">%1$s</a>', $matches[0]);
|
||||
},
|
||||
$text
|
||||
);
|
||||
|
||||
$text = preg_replace_callback(self::MARKUP_USERID, function ($matches) {
|
||||
$getInfo = DB::prepare('
|
||||
SELECT
|
||||
u.`user_id`, u.`username`,
|
||||
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
|
||||
FROM `msz_users` as u
|
||||
LEFT JOIN `msz_roles` as r
|
||||
ON u.`display_role` = r.`role_id`
|
||||
WHERE `user_id` = :user_id
|
||||
');
|
||||
$getInfo->bind('user_id', $matches[1]);
|
||||
$info = $getInfo->fetch();
|
||||
|
||||
if(empty($info))
|
||||
return $matches[0];
|
||||
|
||||
return sprintf(
|
||||
'<a href="%s" class="comment__mention", style="%s">@%s</a>',
|
||||
url('user-profile', ['user' => $info['user_id']]),
|
||||
html_colour($info['user_colour']),
|
||||
$info['username']
|
||||
);
|
||||
}, $text);
|
||||
|
||||
return nl2br($text);
|
||||
}
|
||||
}
|
349
src/Comments/CommentsPost.php
Normal file
349
src/Comments/CommentsPost.php
Normal file
|
@ -0,0 +1,349 @@
|
|||
<?php
|
||||
namespace Misuzu\Comments;
|
||||
|
||||
use JsonSerializable;
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserNotFoundException;
|
||||
|
||||
class CommentsPostException extends CommentsException {}
|
||||
class CommentsPostNotFoundException extends CommentsPostException {}
|
||||
class CommentsPostHasNoParentException extends CommentsPostException {}
|
||||
class CommentsPostSaveFailedException extends CommentsPostException {}
|
||||
|
||||
class CommentsPost implements JsonSerializable {
|
||||
// Database fields
|
||||
private $comment_id = -1;
|
||||
private $category_id = -1;
|
||||
private $user_id = null;
|
||||
private $comment_reply_to = null;
|
||||
private $comment_text = '';
|
||||
private $comment_created = null;
|
||||
private $comment_pinned = null;
|
||||
private $comment_edited = null;
|
||||
private $comment_deleted = null;
|
||||
|
||||
// Virtual fields
|
||||
private $comment_likes = -1;
|
||||
private $comment_dislikes = -1;
|
||||
private $user_vote = null;
|
||||
|
||||
private $category = null;
|
||||
private $user = null;
|
||||
private $userLookedUp = false;
|
||||
private $parentPost = null;
|
||||
|
||||
public const TABLE = 'comments_posts';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`comment_id`, %1$s.`category_id`, %1$s.`user_id`, %1$s.`comment_reply_to`, %1$s.`comment_text`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`comment_created`) AS `comment_created`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`comment_pinned`) AS `comment_pinned`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`comment_edited`) AS `comment_edited`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`comment_deleted`) AS `comment_deleted`';
|
||||
private const LIKE_VOTE_SELECT = '(SELECT COUNT(`comment_id`) FROM `' . DB::PREFIX . CommentsVote::TABLE . '` WHERE `comment_id` = %1$s.`comment_id` AND `comment_vote` = ' . CommentsVote::LIKE . ') AS `comment_likes`';
|
||||
private const DISLIKE_VOTE_SELECT = '(SELECT COUNT(`comment_id`) FROM `' . DB::PREFIX . CommentsVote::TABLE . '` WHERE `comment_id` = %1$s.`comment_id` AND `comment_vote` = ' . CommentsVote::DISLIKE . ') AS `comment_dislikes`';
|
||||
private const USER_VOTE_SELECT = '(SELECT `comment_vote` FROM `' . DB::PREFIX . CommentsVote::TABLE . '` WHERE `comment_id` = %1$s.`comment_id` AND `user_id` = :user) AS `user_vote`';
|
||||
|
||||
public function getId(): int {
|
||||
return $this->comment_id < 1 ? -1 : $this->comment_id;
|
||||
}
|
||||
|
||||
public function getCategoryId(): int {
|
||||
return $this->category_id < 1 ? -1 : $this->category_id;
|
||||
}
|
||||
public function setCategoryId(int $categoryId): self {
|
||||
$this->category_id = $categoryId;
|
||||
$this->category = null;
|
||||
return $this;
|
||||
}
|
||||
public function getCategory(): CommentsCategory {
|
||||
if($this->category === null)
|
||||
$this->category = CommentsCategory::byId($this->getCategoryId());
|
||||
return $this->category;
|
||||
}
|
||||
public function setCategory(CommentsCategory $category): self {
|
||||
$this->category_id = $category->getId();
|
||||
$this->category = null;
|
||||
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->userLookedUp && ($userId = $this->getUserId()) > 0) {
|
||||
$this->userLookedUp = true;
|
||||
try {
|
||||
$this->user = User::byId($userId);
|
||||
} catch(UserNotFoundException $ex) {}
|
||||
}
|
||||
return $this->user;
|
||||
}
|
||||
public function setUser(?User $user): self {
|
||||
$this->user_id = $user === null ? null : $user->getId();
|
||||
$this->user = $user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentId(): int {
|
||||
return $this->comment_reply_to < 1 ? -1 : $this->comment_reply_to;
|
||||
}
|
||||
public function setParentId(int $parentId): self {
|
||||
$this->comment_reply_to = $parentId < 1 ? null : $parentId;
|
||||
$this->parentPost = null;
|
||||
return $this;
|
||||
}
|
||||
public function hasParent(): bool {
|
||||
return $this->getParentId() > 0;
|
||||
}
|
||||
public function getParent(): CommentsPost {
|
||||
if(!$this->hasParent())
|
||||
throw new CommentsPostHasNoParentException;
|
||||
if($this->parentPost === null)
|
||||
$this->parentPost = CommentsPost::byId($this->getParentId());
|
||||
return $this->parentPost;
|
||||
}
|
||||
public function setParent(?CommentsPost $parent): self {
|
||||
$this->comment_reply_to = $parent === null ? null : $parent->getId();
|
||||
$this->parentPost = $parent;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getText(): string {
|
||||
return $this->comment_text;
|
||||
}
|
||||
public function setText(string $text): self {
|
||||
$this->comment_text = $text;
|
||||
return $this;
|
||||
}
|
||||
public function getParsedText(): string {
|
||||
return CommentsParser::parseForDisplay($this->getText());
|
||||
}
|
||||
public function setParsedText(string $text): self {
|
||||
return $this->setText(CommentsParser::parseForStorage($text));
|
||||
}
|
||||
|
||||
public function getCreatedTime(): int {
|
||||
return $this->comment_created === null ? -1 : $this->comment_created;
|
||||
}
|
||||
|
||||
public function getPinnedTime(): int {
|
||||
return $this->comment_pinned === null ? -1 : $this->comment_pinned;
|
||||
}
|
||||
public function isPinned(): bool {
|
||||
return $this->getPinnedTime() >= 0;
|
||||
}
|
||||
public function setPinned(bool $pinned): self {
|
||||
if($this->isPinned() !== $pinned)
|
||||
$this->comment_pinned = $pinned ? time() : null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEditedTime(): int {
|
||||
return $this->comment_edited === null ? -1 : $this->comment_edited;
|
||||
}
|
||||
public function isEdited(): bool {
|
||||
return $this->getEditedTime() >= 0;
|
||||
}
|
||||
|
||||
public function getDeletedTime(): int {
|
||||
return $this->comment_deleted === null ? -1 : $this->comment_deleted;
|
||||
}
|
||||
public function isDeleted(): bool {
|
||||
return $this->getDeletedTime() >= 0;
|
||||
}
|
||||
public function setDeleted(bool $deleted): self {
|
||||
if($this->isDeleted() !== $deleted)
|
||||
$this->comment_deleted = $deleted ? time() : null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLikes(): int {
|
||||
return $this->comment_likes;
|
||||
}
|
||||
public function getDislikes(): int {
|
||||
return $this->comment_dislikes;
|
||||
}
|
||||
|
||||
public function hasUserVote(): bool {
|
||||
return $this->user_vote !== null;
|
||||
}
|
||||
public function getUserVote(): int {
|
||||
return $this->user_vote ?? 0;
|
||||
}
|
||||
|
||||
public function jsonSerialize() {
|
||||
$json = [
|
||||
'id' => $this->getId(),
|
||||
'category' => $this->getCategoryId(),
|
||||
'user' => $this->getUserId(),
|
||||
'parent' => ($parent = $this->getParentId()) < 1 ? null : $parent,
|
||||
'text' => $this->getText(),
|
||||
'created' => ($created = $this->getCreatedTime()) < 0 ? null : date('c', $created),
|
||||
'pinned' => ($pinned = $this->getPinnedTime()) < 0 ? null : date('c', $pinned),
|
||||
'edited' => ($edited = $this->getEditedTime()) < 0 ? null : date('c', $edited),
|
||||
'deleted' => ($deleted = $this->getDeletedTime()) < 0 ? null : date('c', $deleted),
|
||||
];
|
||||
|
||||
if(($likes = $this->getLikes()) >= 0)
|
||||
$json['likes'] = $likes;
|
||||
if(($dislikes = $this->getDislikes()) >= 0)
|
||||
$json['dislikes'] = $dislikes;
|
||||
|
||||
if($this->hasUserVote())
|
||||
$json['user_vote'] = $this->getUserVote();
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
public function save(): void {
|
||||
$isInsert = $this->getId() < 1;
|
||||
if($isInsert) {
|
||||
$query = 'INSERT INTO `%1$s%2$s` (`category_id`, `user_id`, `comment_reply_to`, `comment_text`'
|
||||
. ', `comment_pinned`, `comment_deleted`) VALUES'
|
||||
. ' (:category, :user, :parent, :text, FROM_UNIXTIME(:pinned), FROM_UNIXTIME(:deleted))';
|
||||
} else {
|
||||
$query = 'UPDATE `%1$s%2$s` SET `category_id` = :category, `user_id` = :user, `comment_reply_to` = :parent'
|
||||
. ', `comment_text` = :text, `comment_pinned` = FROM_UNIXTIME(:pinned), `comment_deleted` = FROM_UNIXTIME(:deleted)'
|
||||
. ' WHERE `comment_id` = :post';
|
||||
}
|
||||
|
||||
$savePost = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
|
||||
->bind('category', $this->category_id)
|
||||
->bind('user', $this->user_id)
|
||||
->bind('parent', $this->comment_reply_to)
|
||||
->bind('text', $this->comment_text)
|
||||
->bind('pinned', $this->comment_pinned)
|
||||
->bind('deleted', $this->comment_deleted);
|
||||
|
||||
if($isInsert) {
|
||||
$this->comment_id = $savePost->executeGetId();
|
||||
if($this->comment_id < 1)
|
||||
throw new CommentsPostSaveFailedException;
|
||||
$this->comment_created = time();
|
||||
} else {
|
||||
$this->comment_edited = time();
|
||||
$savePost->bind('post', $this->getId());
|
||||
if(!$savePost->execute())
|
||||
throw new CommentsPostSaveFailedException;
|
||||
}
|
||||
}
|
||||
|
||||
public function nuke(): void {
|
||||
$replies = $this->replies(null, true);
|
||||
foreach($replies as $reply)
|
||||
$reply->nuke();
|
||||
DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `comment_id` = :comment')
|
||||
->bind('comment_id', $this->getId())
|
||||
->execute();
|
||||
}
|
||||
|
||||
public function replies(?User $voteUser = null, bool $includeVotes = true, ?Pagination $pagination = null, bool $includeDeleted = true): array {
|
||||
return CommentsPost::byParent($this, $voteUser, $includeVotes, $pagination, $includeDeleted);
|
||||
}
|
||||
public function votes(): CommentsVoteCount {
|
||||
return CommentsVote::countByPost($this);
|
||||
}
|
||||
public function childVotes(?User $user = null, ?Pagination $pagination = null): array {
|
||||
return CommentsVote::byParent($this, $user, $pagination);
|
||||
}
|
||||
|
||||
public function addPositiveVote(User $user): void {
|
||||
CommentsVote::create($this, $user, CommentsVote::LIKE);
|
||||
}
|
||||
public function addNegativeVote(User $user): void {
|
||||
CommentsVote::create($this, $user, CommentsVote::DISLIKE);
|
||||
}
|
||||
public function removeVote(User $user): void {
|
||||
CommentsVote::delete($this, $user);
|
||||
}
|
||||
|
||||
public function getVoteFromUser(User $user): CommentsVote {
|
||||
return CommentsVote::byExact($this, $user);
|
||||
}
|
||||
|
||||
private static function byQueryBase(bool $includeVotes = true, bool $includeUserVote = false): string {
|
||||
$select = self::SELECT;
|
||||
if($includeVotes)
|
||||
$select .= ', ' . self::LIKE_VOTE_SELECT
|
||||
. ', ' . self::DISLIKE_VOTE_SELECT;
|
||||
if($includeUserVote)
|
||||
$select .= ', ' . self::USER_VOTE_SELECT;
|
||||
return sprintf(self::QUERY_SELECT, sprintf($select, self::TABLE));
|
||||
}
|
||||
public static function byId(int $postId): self {
|
||||
$getPost = DB::prepare(self::byQueryBase() . ' WHERE `comment_id` = :post_id');
|
||||
$getPost->bind('post_id', $postId);
|
||||
$post = $getPost->fetchObject(self::class);
|
||||
if(!$post)
|
||||
throw new CommentsPostNotFoundException;
|
||||
return $post;
|
||||
}
|
||||
public static function byCategory(CommentsCategory $category, ?User $voteUser = null, bool $includeVotes = true, ?Pagination $pagination = null, bool $rootOnly = true, bool $includeDeleted = true): array {
|
||||
$postsQuery = self::byQueryBase($includeVotes, $voteUser !== null)
|
||||
. ' WHERE `category_id` = :category'
|
||||
. (!$rootOnly ? '' : ' AND `comment_reply_to` IS NULL')
|
||||
. ($includeDeleted ? '' : ' AND `comment_deleted` IS NULL')
|
||||
. ' ORDER BY `comment_deleted` ASC, `comment_pinned` DESC, `comment_id` DESC';
|
||||
|
||||
if($pagination !== null)
|
||||
$postsQuery .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getPosts = DB::prepare($postsQuery)
|
||||
->bind('category', $category->getId());
|
||||
|
||||
if($voteUser !== null)
|
||||
$getPosts->bind('user', $voteUser->getId());
|
||||
|
||||
if($pagination !== null)
|
||||
$getPosts->bind('range', $pagination->getRange())
|
||||
->bind('offset', $pagination->getOffset());
|
||||
|
||||
return $getPosts->fetchObjects(self::class);
|
||||
}
|
||||
public static function byParent(CommentsPost $parent, ?User $voteUser = null, bool $includeVotes = true, ?Pagination $pagination = null, bool $includeDeleted = true): array {
|
||||
$postsQuery = self::byQueryBase($includeVotes, $voteUser !== null)
|
||||
. ' WHERE `comment_reply_to` = :parent'
|
||||
. ($includeDeleted ? '' : ' AND `comment_deleted` IS NULL')
|
||||
. ' ORDER BY `comment_deleted` ASC, `comment_pinned` DESC, `comment_id` ASC';
|
||||
|
||||
if($pagination !== null)
|
||||
$postsQuery .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getPosts = DB::prepare($postsQuery)
|
||||
->bind('parent', $parent->getId());
|
||||
|
||||
if($voteUser !== null)
|
||||
$getPosts->bind('user', $voteUser->getId());
|
||||
|
||||
if($pagination !== null)
|
||||
$getPosts->bind('range', $pagination->getRange())
|
||||
->bind('offset', $pagination->getOffset());
|
||||
|
||||
return $getPosts->fetchObjects(self::class);
|
||||
}
|
||||
public static function all(?Pagination $pagination = null, bool $rootOnly = true, bool $includeDeleted = false): array {
|
||||
$postsQuery = self::byQueryBase()
|
||||
. ' WHERE 1' // this is disgusting
|
||||
. (!$rootOnly ? '' : ' AND `comment_reply_to` IS NULL')
|
||||
. ($includeDeleted ? '' : ' AND `comment_deleted` IS NULL')
|
||||
. ' ORDER BY `comment_id` DESC';
|
||||
|
||||
if($pagination !== null)
|
||||
$postsQuery .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getPosts = DB::prepare($postsQuery);
|
||||
|
||||
if($pagination !== null)
|
||||
$getPosts->bind('range', $pagination->getRange())
|
||||
->bind('offset', $pagination->getOffset());
|
||||
|
||||
return $getPosts->fetchObjects(self::class);
|
||||
}
|
||||
}
|
246
src/Comments/CommentsVote.php
Normal file
246
src/Comments/CommentsVote.php
Normal file
|
@ -0,0 +1,246 @@
|
|||
<?php
|
||||
namespace Misuzu\Comments;
|
||||
|
||||
use JsonSerializable;
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class CommentsVoteException extends CommentsException {}
|
||||
class CommentsVoteCountFailedException extends CommentsVoteException {}
|
||||
class CommentsVoteCreateFailedException extends CommentsVoteException {}
|
||||
|
||||
class CommentsVoteCount implements JsonSerializable {
|
||||
private $comment_id = -1;
|
||||
private $likes = 0;
|
||||
private $dislikes = 0;
|
||||
private $total = 0;
|
||||
|
||||
public function getPostId(): int {
|
||||
return $this->comment_id < 1 ? -1 : $this->comment_id;
|
||||
}
|
||||
public function getLikes(): int {
|
||||
return $this->likes;
|
||||
}
|
||||
public function getDislikes(): int {
|
||||
return $this->dislikes;
|
||||
}
|
||||
public function getTotal(): int {
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
public function jsonSerialize() {
|
||||
return [
|
||||
'id' => $this->getPostId(),
|
||||
'likes' => $this->getLikes(),
|
||||
'dislikes' => $this->getDislikes(),
|
||||
'total' => $this->getTotal(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class CommentsVote implements JsonSerializable {
|
||||
// Database fields
|
||||
private $comment_id = -1;
|
||||
private $user_id = -1;
|
||||
private $comment_vote = 0;
|
||||
|
||||
private $comment = null;
|
||||
private $user = null;
|
||||
|
||||
public const LIKE = 1;
|
||||
public const NONE = 0;
|
||||
public const DISLIKE = -1;
|
||||
|
||||
public const TABLE = 'comments_votes';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`comment_id`, %1$s.`user_id`, %1$s.`comment_vote`';
|
||||
|
||||
private const QUERY_COUNT = 'SELECT %3$d AS `comment_id`'
|
||||
. ', (SELECT COUNT(`comment_id`) FROM `%1$s%2$s` WHERE %6$s) AS `total`'
|
||||
. ', (SELECT COUNT(`comment_id`) FROM `%1$s%2$s` WHERE %6$s AND `comment_vote` = %4$d) AS `likes`'
|
||||
. ', (SELECT COUNT(`comment_id`) FROM `%1$s%2$s` WHERE %6$s AND `comment_vote` = %5$d) AS `dislikes`';
|
||||
|
||||
public function getPostId(): int {
|
||||
return $this->comment_id < 1 ? -1 : $this->comment_id;
|
||||
}
|
||||
public function getPost(): CommentsPost {
|
||||
if($this->comment === null)
|
||||
$this->comment = CommentsPost::byId($this->comment_id);
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
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->user_id);
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getVote(): int {
|
||||
return $this->comment_vote;
|
||||
}
|
||||
|
||||
public function jsonSerialize() {
|
||||
return [
|
||||
'post' => $this->getPostId(),
|
||||
'user' => $this->getUserId(),
|
||||
'vote' => $this->getVote(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function create(CommentsPost $post, User $user, int $vote, bool $return = false): ?self {
|
||||
$createVote = DB::prepare('
|
||||
REPLACE INTO `msz_comments_votes`
|
||||
(`comment_id`, `user_id`, `comment_vote`)
|
||||
VALUES
|
||||
(:post, :user, :vote)
|
||||
') ->bind('post', $post->getId())
|
||||
->bind('user', $user->getId())
|
||||
->bind('vote', $vote);
|
||||
|
||||
if(!$createVote->execute())
|
||||
throw new CommentsVoteCreateFailedException;
|
||||
if(!$return)
|
||||
return null;
|
||||
|
||||
return CommentsVote::byExact($post, $user);
|
||||
}
|
||||
|
||||
public static function delete(CommentsPost $post, User $user): void {
|
||||
DB::prepare('DELETE FROM `msz_comments_votes` WHERE `comment_id` = :post AND `user_id` = :user')
|
||||
->bind('post', $post->getId())
|
||||
->bind('user', $user->getId())
|
||||
->execute();
|
||||
}
|
||||
|
||||
private static function countQueryBase(int $id, string $condition = '1'): string {
|
||||
return sprintf(self::QUERY_COUNT, DB::PREFIX, self::TABLE, $id, self::LIKE, self::DISLIKE, $condition);
|
||||
}
|
||||
public static function countByPost(CommentsPost $post): CommentsVoteCount {
|
||||
$count = DB::prepare(self::countQueryBase($post->getId(), sprintf('`comment_id` = %d', $post->getId())))
|
||||
->fetchObject(CommentsVoteCount::class);
|
||||
if(!$count)
|
||||
throw new CommentsVoteCountFailedException;
|
||||
return $count;
|
||||
}
|
||||
|
||||
private static function fake(CommentsPost $post, User $user, int $vote): CommentsVote {
|
||||
$fake = new static;
|
||||
$fake->comment_id = $post->getId();
|
||||
$fake->comment = $post;
|
||||
$fake->user_id = $user->getId();
|
||||
$fake->user = $user;
|
||||
$fake->comment_vote = $vote;
|
||||
return $fake;
|
||||
}
|
||||
|
||||
private static function byQueryBase(): string {
|
||||
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
|
||||
}
|
||||
public static function byExact(CommentsPost $post, User $user): self {
|
||||
$vote = DB::prepare(self::byQueryBase() . ' WHERE `comment_id` = :post_id AND `user_id` = :user_id')
|
||||
->bind('post_id', $post->getId())
|
||||
->bind('user_id', $user->getId())
|
||||
->fetchObject(self::class);
|
||||
if(!$vote)
|
||||
return self::fake($post, $user, self::NONE);
|
||||
return $vote;
|
||||
}
|
||||
public static function byPost(CommentsPost $post, ?User $user = null, ?Pagination $pagination = null): array {
|
||||
$votesQuery = self::byQueryBase()
|
||||
. ' WHERE `comment_id` = :post'
|
||||
. ($user === null ? '' : ' AND `user_id` = :user');
|
||||
|
||||
if($pagination !== null)
|
||||
$votesQuery .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getVotes = DB::prepare($votesQuery)
|
||||
->bind('post', $post->getId());
|
||||
|
||||
if($user !== null)
|
||||
$getVotes->bind('user', $user->getId());
|
||||
|
||||
if($pagination !== null)
|
||||
$getVotes->bind('range', $pagination->getRange())
|
||||
->bind('offset', $pagination->getOffset());
|
||||
|
||||
return $getVotes->fetchObjects(self::class);
|
||||
}
|
||||
public static function byUser(User $user, ?Pagination $pagination = null): array {
|
||||
$votesQuery = self::byQueryBase()
|
||||
. ' WHERE `user_id` = :user';
|
||||
|
||||
if($pagination !== null)
|
||||
$votesQuery .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getVotes = DB::prepare($votesQuery)
|
||||
->bind('user', $user->getId());
|
||||
|
||||
if($pagination !== null)
|
||||
$getVotes->bind('range', $pagination->getRange())
|
||||
->bind('offset', $pagination->getOffset());
|
||||
|
||||
return $getVotes->fetchObjects(self::class);
|
||||
}
|
||||
public static function byCategory(CommentsCategory $category, ?User $user = null, bool $rootOnly = true, ?Pagination $pagination = null): array {
|
||||
$votesQuery = self::byQueryBase()
|
||||
. ' WHERE `comment_id` IN'
|
||||
. ' (SELECT `comment_id` FROM `' . DB::PREFIX . CommentsPost::TABLE . '` WHERE `category_id` = :category'
|
||||
. (!$rootOnly ? '' : ' AND `comment_reply_to` IS NULL')
|
||||
. ')'
|
||||
. ($user === null ? '' : ' AND `user_id` = :user');
|
||||
|
||||
if($pagination !== null)
|
||||
$votesQuery .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getVotes = DB::prepare($votesQuery)
|
||||
->bind('category', $category->getId());
|
||||
|
||||
if($user !== null)
|
||||
$getVotes->bind('user', $user->getId());
|
||||
|
||||
if($pagination !== null)
|
||||
$getVotes->bind('range', $pagination->getRange())
|
||||
->bind('offset', $pagination->getOffset());
|
||||
|
||||
return $getVotes->fetchObjects(self::class);
|
||||
}
|
||||
public static function byParent(CommentsPost $parent, ?User $user = null, ?Pagination $pagination = null): array {
|
||||
$votesQuery = self::byQueryBase()
|
||||
. ' WHERE `comment_id` IN'
|
||||
. ' (SELECT `comment_id` FROM `' . DB::PREFIX . CommentsPost::TABLE . '` WHERE `comment_reply_to` = :parent)'
|
||||
. ($user === null ? '' : ' AND `user_id` = :user');
|
||||
|
||||
if($pagination !== null)
|
||||
$votesQuery .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getVotes = DB::prepare($votesQuery)
|
||||
->bind('parent', $parent->getId());
|
||||
|
||||
if($user !== null)
|
||||
$getVotes->bind('user', $user->getId());
|
||||
|
||||
if($pagination !== null)
|
||||
$getVotes->bind('range', $pagination->getRange())
|
||||
->bind('offset', $pagination->getOffset());
|
||||
|
||||
return $getVotes->fetchObjects(self::class);
|
||||
}
|
||||
public static function all(?Pagination $pagination = null): array {
|
||||
$votesQuery = self::byQueryBase();
|
||||
|
||||
if($pagination !== null)
|
||||
$votesQuery .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getVotes = DB::prepare($votesQuery);
|
||||
|
||||
if($pagination !== null)
|
||||
$getVotes->bind('range', $pagination->getRange())
|
||||
->bind('offset', $pagination->getOffset());
|
||||
|
||||
return $getVotes->fetchObjects(self::class);
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ final class DB {
|
|||
private static $instance;
|
||||
|
||||
public const PREFIX = 'msz_';
|
||||
public const QUERY_SELECT = 'SELECT %2$s FROM `' . self::PREFIX . '%1$s` AS %1$s';
|
||||
|
||||
public const ATTRS = [
|
||||
PDO::ATTR_CASE => PDO::CASE_NATURAL,
|
||||
|
@ -16,11 +15,8 @@ final class DB {
|
|||
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
|
||||
PDO::ATTR_STRINGIFY_FETCHES => false,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "
|
||||
SET SESSION
|
||||
sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION',
|
||||
time_zone = '+00:00';
|
||||
",
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET SESSION time_zone = \'+00:00\''
|
||||
. ', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\'',
|
||||
];
|
||||
|
||||
public static function init(...$args) {
|
||||
|
|
|
@ -1,54 +1,63 @@
|
|||
<?php
|
||||
namespace Misuzu\Debug;
|
||||
|
||||
class Stopwatch {
|
||||
final class Stopwatch {
|
||||
private $startTime = 0;
|
||||
private $stopTime = 0;
|
||||
private $laps = [];
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function __callStatic(string $name, array $args) {
|
||||
if(self::$instance === null)
|
||||
self::$instance = new static;
|
||||
return self::$instance->{substr($name, 1)}(...$args);
|
||||
public function __call(string $name, array $args) {
|
||||
if($name[0] === '_')
|
||||
return null;
|
||||
return $this->{'_' . $name}(...$args);
|
||||
}
|
||||
|
||||
public function __construct() {}
|
||||
public static function __callStatic(string $name, array $args) {
|
||||
if($name[0] === '_')
|
||||
return null;
|
||||
if(self::$instance === null)
|
||||
self::$instance = new static;
|
||||
return self::$instance->{'_' . $name}(...$args);
|
||||
}
|
||||
|
||||
private static function time() {
|
||||
return microtime(true);
|
||||
}
|
||||
|
||||
public function start(): void {
|
||||
public function _start(): void {
|
||||
$this->startTime = self::time();
|
||||
}
|
||||
|
||||
public function lap(string $text): void {
|
||||
public function _lap(string $text): void {
|
||||
$this->laps[$text] = self::time();
|
||||
}
|
||||
|
||||
public function stop(): void {
|
||||
public function _stop(): void {
|
||||
$this->stopTime = self::time();
|
||||
}
|
||||
|
||||
public function reset(): void {
|
||||
public function _reset(): void {
|
||||
$this->laps = [];
|
||||
$this->startTime = 0;
|
||||
$this->stopTime = 0;
|
||||
}
|
||||
|
||||
public function elapsed(): float {
|
||||
public function _elapsed(): float {
|
||||
return $this->stopTime - $this->startTime;
|
||||
}
|
||||
|
||||
public function laps(): array {
|
||||
public function _laps(): array {
|
||||
$laps = [];
|
||||
|
||||
foreach($this->laps as $name => $time) {
|
||||
foreach($this->laps as $name => $time)
|
||||
$laps[$name] = $time - $this->startTime;
|
||||
}
|
||||
|
||||
return $laps;
|
||||
}
|
||||
|
||||
public function _dump(bool $trimmed = false): void {
|
||||
header('X-Misuzu-Elapsed: ' . $this->_elapsed());
|
||||
foreach($this->_laps() as $text => $time)
|
||||
header('X-Misuzu-Lap: ' . ($trimmed ? number_format($time, 6) : $time) . ' ' . $text, false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ use Misuzu\News\NewsPost;
|
|||
use Misuzu\News\NewsCategoryNotFoundException;
|
||||
use Misuzu\News\NewsPostNotException;
|
||||
use Misuzu\Parsers\Parser;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserNotFoundException;
|
||||
|
||||
final class NewsHandler extends Handler {
|
||||
public function index(HttpResponse $response, HttpRequest $request) {
|
||||
|
@ -42,11 +44,9 @@ final class NewsHandler extends Handler {
|
|||
if(!$categoryPagination->hasValidOffset())
|
||||
return 404;
|
||||
|
||||
$posts = NewsPost::byCategory($categoryInfo, $categoryPagination);
|
||||
|
||||
$response->setTemplate('news.category', [
|
||||
'category_info' => $categoryInfo,
|
||||
'posts' => $posts,
|
||||
'posts' => $categoryInfo->posts($categoryPagination),
|
||||
'news_pagination' => $categoryPagination,
|
||||
]);
|
||||
}
|
||||
|
@ -63,12 +63,16 @@ final class NewsHandler extends Handler {
|
|||
|
||||
$postInfo->ensureCommentsSection();
|
||||
$commentsInfo = $postInfo->getCommentSection();
|
||||
try {
|
||||
$commentsUser = User::byId(user_session_current('user_id', 0));
|
||||
} catch(UserNotFoundException $ex) {
|
||||
$commentsUser = null;
|
||||
}
|
||||
|
||||
$response->setTemplate('news.post', [
|
||||
'post_info' => $postInfo,
|
||||
'comments_perms' => comments_get_perms(user_session_current('user_id', 0)),
|
||||
'comments_category' => $commentsInfo,
|
||||
'comments' => comments_category_get($commentsInfo['category_id'], user_session_current('user_id', 0)),
|
||||
'comments_info' => $commentsInfo,
|
||||
'comments_user' => $commentsUser,
|
||||
]);
|
||||
|
||||
}
|
||||
|
@ -76,7 +80,7 @@ final class NewsHandler extends Handler {
|
|||
private function createFeed(string $feedMode, ?NewsCategory $categoryInfo, array $posts): Feed {
|
||||
$hasCategory = !empty($categoryInfo);
|
||||
$pagination = new Pagination(10);
|
||||
$posts = $hasCategory ? NewsPost::byCategory($categoryInfo, $pagination) : NewsPost::all($pagination, true);
|
||||
$posts = $hasCategory ? $categoryInfo->posts($pagination) : NewsPost::all($pagination, true);
|
||||
|
||||
$feed = (new Feed)
|
||||
->setTitle(Config::get('site.name', Config::TYPE_STR, 'Misuzu') . ' » ' . ($hasCategory ? $categoryInfo->getName() : 'Featured News'))
|
||||
|
@ -132,7 +136,7 @@ final class NewsHandler extends Handler {
|
|||
|
||||
$response->setContentType('application/atom+xml; charset=utf-8');
|
||||
return (new AtomFeedSerializer)->serializeFeed(
|
||||
self::createFeed('atom', $categoryInfo, NewsPost::byCategory($categoryInfo, new Pagination(10)))
|
||||
self::createFeed('atom', $categoryInfo, $categoryInfo->posts(new Pagination(10)))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -145,7 +149,7 @@ final class NewsHandler extends Handler {
|
|||
|
||||
$response->setContentType('application/rss+xml; charset=utf-8');
|
||||
return (new RssFeedSerializer)->serializeFeed(
|
||||
self::createFeed('rss', $categoryInfo, NewsPost::byCategory($categoryInfo, new Pagination(10)))
|
||||
self::createFeed('rss', $categoryInfo, $categoryInfo->posts(new Pagination(10)))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -246,29 +246,29 @@ final class SockChatHandler extends Handler {
|
|||
if(!isset($userId) || $userId < 1)
|
||||
return ['success' => false, 'reason' => 'unknown'];
|
||||
|
||||
$userInfo = User::get($userId);
|
||||
$userInfo = User::byId($userId);
|
||||
|
||||
if($userInfo === null || !$userInfo->hasUserId())
|
||||
if($userInfo === null)
|
||||
return ['success' => false, 'reason' => 'user'];
|
||||
|
||||
$perms = self::PERMS_DEFAULT;
|
||||
|
||||
if(perms_check_user(MSZ_PERMS_USER, $userInfo->user_id, MSZ_PERM_USER_MANAGE_USERS))
|
||||
if(perms_check_user(MSZ_PERMS_USER, $userInfo->getId(), MSZ_PERM_USER_MANAGE_USERS))
|
||||
$perms |= self::PERMS_MANAGE_USERS;
|
||||
if(perms_check_user(MSZ_PERMS_USER, $userInfo->user_id, MSZ_PERM_USER_MANAGE_WARNINGS))
|
||||
if(perms_check_user(MSZ_PERMS_USER, $userInfo->getId(), MSZ_PERM_USER_MANAGE_WARNINGS))
|
||||
$perms |= self::PERMS_MANAGE_WARNS;
|
||||
if(perms_check_user(MSZ_PERMS_USER, $userInfo->user_id, MSZ_PERM_USER_CHANGE_BACKGROUND))
|
||||
if(perms_check_user(MSZ_PERMS_USER, $userInfo->getId(), MSZ_PERM_USER_CHANGE_BACKGROUND))
|
||||
$perms |= self::PERMS_CHANGE_BACKG;
|
||||
if(perms_check_user(MSZ_PERMS_FORUM, $userInfo->user_id, MSZ_PERM_FORUM_MANAGE_FORUMS))
|
||||
if(perms_check_user(MSZ_PERMS_FORUM, $userInfo->getId(), MSZ_PERM_FORUM_MANAGE_FORUMS))
|
||||
$perms |= self::PERMS_MANAGE_FORUM;
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'user_id' => $userInfo->getUserId(),
|
||||
'user_id' => $userInfo->getId(),
|
||||
'username' => $userInfo->getUsername(),
|
||||
'colour_raw' => $userInfo->getColourRaw(),
|
||||
'hierarchy' => $userInfo->getHierarchy(),
|
||||
'is_silenced' => date('c', user_warning_check_expiration($userInfo->getUserId(), MSZ_WARN_SILENCE)),
|
||||
'is_silenced' => date('c', user_warning_check_expiration($userInfo->getId(), MSZ_WARN_SILENCE)),
|
||||
'perms' => $perms,
|
||||
];
|
||||
}
|
||||
|
|
27
src/Memoizer.php
Normal file
27
src/Memoizer.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class Memoizer {
|
||||
private $collection = [];
|
||||
|
||||
public function find($find, callable $create) {
|
||||
if(is_int($find) || is_string($find)) {
|
||||
if(!isset($this->collection[$find]))
|
||||
$this->collection[$find] = $create();
|
||||
return $this->collection[$find];
|
||||
}
|
||||
|
||||
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;
|
||||
return $item;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Wasn\'t able to figure out your $find argument.');
|
||||
}
|
||||
}
|
|
@ -18,7 +18,8 @@ class NewsCategory implements ArrayAccess {
|
|||
|
||||
private $postCount = -1;
|
||||
|
||||
private const TABLE = 'news_categories';
|
||||
public const TABLE = 'news_categories';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`category_id`, %1$s.`category_name`, %1$s.`category_description`, %1$s.`category_is_hidden`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`category_created`) AS `category_created`';
|
||||
|
||||
|
@ -94,8 +95,12 @@ class NewsCategory implements ArrayAccess {
|
|||
}
|
||||
}
|
||||
|
||||
public function posts(?Pagination $pagination = null, bool $includeScheduled = false, bool $includeDeleted = false): array {
|
||||
return NewsPost::byCategory($this, $pagination, $includeScheduled, $includeDeleted);
|
||||
}
|
||||
|
||||
private static function countQueryBase(): string {
|
||||
return sprintf(DB::QUERY_SELECT, self::TABLE, sprintf('COUNT(%s.`category_id`)', self::TABLE));
|
||||
return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`category_id`)', self::TABLE));
|
||||
}
|
||||
public static function countAll(bool $showHidden = false): int {
|
||||
return (int)DB::prepare(self::countQueryBase()
|
||||
|
@ -104,7 +109,7 @@ class NewsCategory implements ArrayAccess {
|
|||
}
|
||||
|
||||
private static function byQueryBase(): string {
|
||||
return sprintf(DB::QUERY_SELECT, self::TABLE, sprintf(self::SELECT, self::TABLE));
|
||||
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
|
||||
}
|
||||
public static function byId(int $categoryId): self {
|
||||
$getCat = DB::prepare(self::byQueryBase() . ' WHERE `category_id` = :cat_id');
|
||||
|
|
|
@ -3,7 +3,10 @@ namespace Misuzu\News;
|
|||
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Comments\CommentsCategory;
|
||||
use Misuzu\Comments\CommentsCategoryNotFoundException;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserNotFoundException;
|
||||
|
||||
class NewsPostException extends NewsException {};
|
||||
class NewsPostNotFoundException extends NewsPostException {};
|
||||
|
@ -24,10 +27,11 @@ class NewsPost {
|
|||
|
||||
private $category = null;
|
||||
private $user = null;
|
||||
private $userLookedUp = false;
|
||||
private $comments = null;
|
||||
private $commentCount = -1;
|
||||
|
||||
private const TABLE = 'news_posts';
|
||||
public const TABLE = 'news_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.`category_id`, %1$s.`user_id`, %1$s.`comment_section_id`'
|
||||
. ', %1$s.`post_is_featured`, %1$s.`post_title`, %1$s.`post_text`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`post_scheduled`) AS `post_scheduled`'
|
||||
|
@ -46,15 +50,17 @@ class NewsPost {
|
|||
}
|
||||
public function setCategoryId(int $categoryId): self {
|
||||
$this->category_id = max(1, $categoryId);
|
||||
$this->category = null;
|
||||
return $this;
|
||||
}
|
||||
public function getCategory(): NewsCategory {
|
||||
if($this->category === null && ($catId = $this->getCategoryId()) > 0)
|
||||
$this->category = NewsCategory::byId($catId);
|
||||
if($this->category === null)
|
||||
$this->category = NewsCategory::byId($this->getCategoryId());
|
||||
return $this->category;
|
||||
}
|
||||
public function setCategory(NewsCategory $category): self {
|
||||
$this->category_id = $category->getId();
|
||||
$this->category = $category;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -63,41 +69,35 @@ class NewsPost {
|
|||
}
|
||||
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);
|
||||
if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) {
|
||||
$this->userLookedUp = true;
|
||||
try {
|
||||
$this->user = User::byId($userId);
|
||||
} catch(UserNotFoundException $ex) {}
|
||||
}
|
||||
return $this->user;
|
||||
}
|
||||
public function setUser(?User $user): self {
|
||||
$this->user_id = $user === null ? null : $user->getId();
|
||||
$this->user = $user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCommentSectionId(): int {
|
||||
return $this->comment_section_id < 1 ? -1 : $this->comment_section_id;
|
||||
}
|
||||
public function hasCommentsSection(): bool {
|
||||
public function hasCommentSection(): bool {
|
||||
return $this->getCommentSectionId() > 0;
|
||||
}
|
||||
public function getCommentSection() {
|
||||
if($this->comments === null && ($sectionId = $this->getCommentSectionId()) > 0)
|
||||
$this->comments = comments_category_info($sectionId);
|
||||
public function getCommentSection(): CommentsCategory {
|
||||
if($this->comments === null)
|
||||
$this->comments = CommentsCategory::byId($this->getCommentSectionId());
|
||||
return $this->comments;
|
||||
}
|
||||
// Temporary solution, should be a method of whatever getCommentSection returns
|
||||
public function getCommentCount(): int {
|
||||
if($this->commentCount < 0)
|
||||
$this->commentCount = (int)DB::prepare('
|
||||
SELECT COUNT(`comment_id`)
|
||||
FROM `msz_comments_posts`
|
||||
WHERE `category_id` = :cat_id
|
||||
AND `comment_deleted` IS NULL
|
||||
')->bind('cat_id', $this->getCommentSectionId())->fetchColumn();
|
||||
|
||||
return $this->commentCount;
|
||||
}
|
||||
|
||||
public function isFeatured(): bool {
|
||||
return $this->post_is_featured !== 0;
|
||||
|
@ -153,24 +153,25 @@ class NewsPost {
|
|||
return $this->getDeletedTime() >= 0;
|
||||
}
|
||||
public function setDeleted(bool $isDeleted): self {
|
||||
$this->post_deleted = $isDeleted ? time() : null;
|
||||
if($this->isDeleted() !== $isDeleted)
|
||||
$this->post_deleted = $isDeleted ? time() : null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function ensureCommentsSection(): void {
|
||||
if($this->hasCommentsSection())
|
||||
if($this->hasCommentSection())
|
||||
return;
|
||||
|
||||
$this->comments = comments_category_create("news-{$this->getId()}");
|
||||
$this->comments = (new CommentsCategory)
|
||||
->setName("news-{$this->getId()}");
|
||||
$this->comments->save();
|
||||
|
||||
if($this->comments !== null) {
|
||||
$this->comment_section_id = (int)$this->comments['category_id'];
|
||||
DB::prepare('UPDATE `msz_news_posts` SET `comment_section_id` = :comment_section_id WHERE `post_id` = :post_id')
|
||||
->execute([
|
||||
'comment_section_id' => $this->getCommentSectionId(),
|
||||
'post_id' => $this->getId(),
|
||||
]);
|
||||
}
|
||||
$this->comment_section_id = $this->comments->getId();
|
||||
DB::prepare('UPDATE `msz_news_posts` SET `comment_section_id` = :comment_section_id WHERE `post_id` = :post_id')
|
||||
->execute([
|
||||
'comment_section_id' => $this->getCommentSectionId(),
|
||||
'post_id' => $this->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function save(): void {
|
||||
|
@ -206,7 +207,7 @@ class NewsPost {
|
|||
}
|
||||
|
||||
private static function countQueryBase(): string {
|
||||
return sprintf(DB::QUERY_SELECT, self::TABLE, sprintf('COUNT(%s.`post_id`)', self::TABLE));
|
||||
return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`post_id`)', self::TABLE));
|
||||
}
|
||||
public static function countAll(bool $onlyFeatured = false, bool $includeScheduled = false, bool $includeDeleted = false): int {
|
||||
return (int)DB::prepare(self::countQueryBase()
|
||||
|
@ -226,7 +227,7 @@ class NewsPost {
|
|||
}
|
||||
|
||||
private static function byQueryBase(): string {
|
||||
return sprintf(DB::QUERY_SELECT, self::TABLE, sprintf(self::SELECT, self::TABLE));
|
||||
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
|
||||
}
|
||||
public static function byId(int $postId): self {
|
||||
$post = DB::prepare(self::byQueryBase() . ' WHERE `post_id` = :post_id')
|
||||
|
|
|
@ -3,9 +3,37 @@ namespace Misuzu\Users;
|
|||
|
||||
use Misuzu\Colour;
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Memoizer;
|
||||
use Misuzu\Net\IPAddress;
|
||||
|
||||
class UserException extends UsersException {} // this naming definitely won't lead to confusion down the line!
|
||||
class UserNotFoundException extends UserException {}
|
||||
|
||||
class User {
|
||||
// Database fields
|
||||
// TODO: update all references to use getters and setters and mark all of these as private
|
||||
public $user_id = -1;
|
||||
public $username = '';
|
||||
public $password = '';
|
||||
public $email = '';
|
||||
public $register_ip = '::1';
|
||||
public $last_ip = '::1';
|
||||
public $user_super = 0;
|
||||
public $user_country = 'XX';
|
||||
public $user_colour = null;
|
||||
public $user_created = null;
|
||||
public $user_active = null;
|
||||
public $user_deleted = null;
|
||||
public $display_role = 1;
|
||||
public $user_totp_key = null;
|
||||
public $user_about_content = null;
|
||||
public $user_about_parser = 0;
|
||||
public $user_signature_content = null;
|
||||
public $user_signature_parser = 0;
|
||||
public $user_birthdate = '';
|
||||
public $user_background_settings = 0;
|
||||
public $user_title = null;
|
||||
|
||||
private const USER_SELECT = '
|
||||
SELECT u.`user_id`, u.`username`, u.`password`, u.`email`, u.`user_super`, u.`user_title`,
|
||||
u.`user_country`, u.`user_colour`, u.`display_role`, u.`user_totp_key`,
|
||||
|
@ -50,48 +78,17 @@ class User {
|
|||
if($createUser < 1)
|
||||
return null;
|
||||
|
||||
return static::get($createUser);
|
||||
return static::byId($createUser);
|
||||
}
|
||||
|
||||
public static function get(int $userId): ?User { return self::byId($userId); }
|
||||
public static function byId(int $userId): ?User {
|
||||
return DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id')
|
||||
->bind('user_id', $userId)
|
||||
->fetchObject(User::class);
|
||||
}
|
||||
|
||||
public static function findForLogin(string $usernameOrEmail): ?User {
|
||||
return DB::prepare(self::USER_SELECT . 'WHERE LOWER(`email`) = LOWER(:email) OR LOWER(`username`) = LOWER(:username)')
|
||||
->bind('email', $usernameOrEmail)
|
||||
->bind('username', $usernameOrEmail)
|
||||
->fetchObject(User::class);
|
||||
}
|
||||
public static function findForProfile($userId): ?User {
|
||||
return DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id OR LOWER(`username`) = LOWER(:username)')
|
||||
->bind('user_id', (int)$userId)
|
||||
->bind('username', (string)$userId)
|
||||
->fetchObject(User::class);
|
||||
}
|
||||
|
||||
public function hasUserId(): bool { return $this->hasId(); }
|
||||
public function getUserId(): int { return $this->getId(); }
|
||||
public function hasId(): bool {
|
||||
return isset($this->user_id) && $this->user_id > 0;
|
||||
}
|
||||
public function getId(): int {
|
||||
return $this->user_id ?? 0;
|
||||
return $this->user_id < 1 ? -1 : $this->user_id;
|
||||
}
|
||||
|
||||
public function hasUsername(): bool {
|
||||
return isset($this->username);
|
||||
}
|
||||
public function getUsername(): string {
|
||||
return $this->username ?? '';
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function hasColour(): bool {
|
||||
return isset($this->user_colour);
|
||||
}
|
||||
public function getColour(): Colour {
|
||||
return new Colour($this->getColourRaw());
|
||||
}
|
||||
|
@ -99,8 +96,12 @@ class User {
|
|||
return $this->user_colour ?? 0x40000000;
|
||||
}
|
||||
|
||||
public function getEmailAddress(): string {
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getHierarchy(): int {
|
||||
return $this->hasUserId() ? user_get_hierarchy($this->getUserId()) : 0;
|
||||
return ($userId = $this->getId()) < 1 ? 0 : user_get_hierarchy($userId);
|
||||
}
|
||||
|
||||
public function hasPassword(): bool {
|
||||
|
@ -113,12 +114,12 @@ class User {
|
|||
return password_needs_rehash($this->password, MSZ_USERS_PASSWORD_HASH_ALGO);
|
||||
}
|
||||
public function setPassword(string $password): void {
|
||||
if(!$this->hasUserId())
|
||||
if(($userId = $this->getId()) < 1)
|
||||
return;
|
||||
|
||||
DB::prepare('UPDATE `msz_users` SET `password` = :password WHERE `user_id` = :user_id')
|
||||
->bind('password', password_hash($password, MSZ_USERS_PASSWORD_HASH_ALGO))
|
||||
->bind('user_id', $this->user_id)
|
||||
->bind('user_id', $userId)
|
||||
->execute();
|
||||
}
|
||||
|
||||
|
@ -130,6 +131,9 @@ class User {
|
|||
return !empty($this->user_totp_key);
|
||||
}
|
||||
|
||||
public function getBackgroundSettings(): int { // Use the below methods instead
|
||||
return $this->user_background_settings;
|
||||
}
|
||||
public function getBackgroundAttachment(): int {
|
||||
return $this->user_background_settings & 0x0F;
|
||||
}
|
||||
|
@ -141,9 +145,70 @@ class User {
|
|||
}
|
||||
|
||||
public function profileFields(bool $filterEmpty = true): array {
|
||||
if(!$this->hasUserId())
|
||||
if(($userId = $this->getId()) < 1)
|
||||
return [];
|
||||
return ProfileField::user($userId, $filterEmpty);
|
||||
}
|
||||
|
||||
return ProfileField::user($this->user_id, $filterEmpty);
|
||||
// TODO: Is this the proper location/implementation for this? (no)
|
||||
private $commentPermsArray = null;
|
||||
public function commentPerms(): array {
|
||||
if($this->commentPermsArray === null)
|
||||
$this->commentPermsArray = perms_check_user_bulk(MSZ_PERMS_COMMENTS, $this->getId(), [
|
||||
'can_comment' => MSZ_PERM_COMMENTS_CREATE,
|
||||
'can_delete' => MSZ_PERM_COMMENTS_DELETE_OWN | MSZ_PERM_COMMENTS_DELETE_ANY,
|
||||
'can_delete_any' => MSZ_PERM_COMMENTS_DELETE_ANY,
|
||||
'can_pin' => MSZ_PERM_COMMENTS_PIN,
|
||||
'can_lock' => MSZ_PERM_COMMENTS_LOCK,
|
||||
'can_vote' => MSZ_PERM_COMMENTS_VOTE,
|
||||
]);
|
||||
return $this->commentPermsArray;
|
||||
}
|
||||
|
||||
private static function getMemoizer() {
|
||||
static $memoizer = null;
|
||||
if($memoizer === null)
|
||||
$memoizer = new Memoizer;
|
||||
return $memoizer;
|
||||
}
|
||||
|
||||
public static function byId(int $userId): ?User {
|
||||
return self::getMemoizer()->find($userId, function() use ($userId) {
|
||||
$user = DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id')
|
||||
->bind('user_id', $userId)
|
||||
->fetchObject(User::class);
|
||||
if(!$user)
|
||||
throw new UserNotFoundException;
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
public static function findForLogin(string $usernameOrEmail): ?User {
|
||||
$usernameOrEmailLower = mb_strtolower($usernameOrEmail);
|
||||
return self::getMemoizer()->find(function() use ($usernameOrEmailLower) {
|
||||
return mb_strtolower($user->getUsername()) === $usernameOrEmailLower
|
||||
|| mb_strtolower($user->getEmailAddress()) === $usernameOrEmailLower;
|
||||
}, function() use ($usernameOrEmail) {
|
||||
$user = DB::prepare(self::USER_SELECT . 'WHERE LOWER(`email`) = LOWER(:email) OR LOWER(`username`) = LOWER(:username)')
|
||||
->bind('email', $usernameOrEmail)
|
||||
->bind('username', $usernameOrEmail)
|
||||
->fetchObject(User::class);
|
||||
if(!$user)
|
||||
throw new UserNotFoundException;
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
public static function findForProfile($userIdOrName): ?User {
|
||||
$userIdOrNameLower = mb_strtolower($userIdOrName);
|
||||
return self::getMemoizer()->find(function() use ($userIdOrNameLower) {
|
||||
return $user->getId() == $userIdOrNameLower || mb_strtolower($user->getUsername()) === $userIdOrNameLower;
|
||||
}, function() use ($userIdOrName) {
|
||||
$user = DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id OR LOWER(`username`) = LOWER(:username)')
|
||||
->bind('user_id', (int)$userIdOrName)
|
||||
->bind('username', (string)$userIdOrName)
|
||||
->fetchObject(User::class);
|
||||
if(!$user)
|
||||
throw new UserNotFoundException;
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
6
src/Users/UsersException.php
Normal file
6
src/Users/UsersException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace Misuzu\Users;
|
||||
|
||||
use Exception;
|
||||
|
||||
class UsersException extends Exception {}
|
364
src/comments.php
364
src/comments.php
|
@ -1,364 +0,0 @@
|
|||
<?php
|
||||
require_once 'Users/validation.php';
|
||||
|
||||
define('MSZ_COMMENTS_VOTE_INDIFFERENT', 0);
|
||||
define('MSZ_COMMENTS_VOTE_LIKE', 1);
|
||||
define('MSZ_COMMENTS_VOTE_DISLIKE', -1);
|
||||
define('MSZ_COMMENTS_VOTE_TYPES', [
|
||||
MSZ_COMMENTS_VOTE_INDIFFERENT,
|
||||
MSZ_COMMENTS_VOTE_LIKE,
|
||||
MSZ_COMMENTS_VOTE_DISLIKE,
|
||||
]);
|
||||
|
||||
// gets parsed on post
|
||||
define('MSZ_COMMENTS_MARKUP_USERNAME', '#\B(?:@{1}(' . MSZ_USERNAME_REGEX . '))#u');
|
||||
|
||||
// gets parsed on fetch
|
||||
define('MSZ_COMMENTS_MARKUP_USER_ID', '#\B(?:@{2}([0-9]+))#u');
|
||||
|
||||
function comments_vote_type_valid(int $voteType): bool {
|
||||
return in_array($voteType, MSZ_COMMENTS_VOTE_TYPES, true);
|
||||
}
|
||||
|
||||
function comments_parse_for_store(string $text): string {
|
||||
return preg_replace_callback(MSZ_COMMENTS_MARKUP_USERNAME, function ($matches) {
|
||||
return ($userId = user_id_from_username($matches[1])) < 1
|
||||
? $matches[0]
|
||||
: "@@{$userId}";
|
||||
}, $text);
|
||||
}
|
||||
|
||||
function comments_parse_for_display(string $text): string {
|
||||
$text = preg_replace_callback(
|
||||
'/(^|[\n ])([\w]*?)([\w]*?:\/\/[\w]+[^ \,\"\n\r\t<]*)/is',
|
||||
function ($matches) {
|
||||
$matches[0] = trim($matches[0]);
|
||||
$url = parse_url($matches[0]);
|
||||
|
||||
if(empty($url['scheme']) || !in_array(mb_strtolower($url['scheme']), ['http', 'https'], true)) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
return sprintf(' <a href="%1$s" class="link" target="_blank" rel="noreferrer noopener">%1$s</a>', $matches[0]);
|
||||
},
|
||||
$text
|
||||
);
|
||||
|
||||
$text = preg_replace_callback(MSZ_COMMENTS_MARKUP_USER_ID, function ($matches) {
|
||||
$getInfo = \Misuzu\DB::prepare('
|
||||
SELECT
|
||||
u.`user_id`, u.`username`,
|
||||
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
|
||||
FROM `msz_users` as u
|
||||
LEFT JOIN `msz_roles` as r
|
||||
ON u.`display_role` = r.`role_id`
|
||||
WHERE `user_id` = :user_id
|
||||
');
|
||||
$getInfo->bind('user_id', $matches[1]);
|
||||
$info = $getInfo->fetch();
|
||||
|
||||
if(empty($info)) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<a href="%s" class="comment__mention", style="%s">@%s</a>',
|
||||
url('user-profile', ['user' => $info['user_id']]),
|
||||
html_colour($info['user_colour']),
|
||||
$info['username']
|
||||
);
|
||||
}, $text);
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
// usually this is not how you're suppose to handle permission checking,
|
||||
// but in the context of comments this is fine since the same shit is used
|
||||
// for every comment section.
|
||||
function comments_get_perms(int $userId): array {
|
||||
return perms_check_user_bulk(MSZ_PERMS_COMMENTS, $userId, [
|
||||
'can_comment' => MSZ_PERM_COMMENTS_CREATE,
|
||||
'can_delete' => MSZ_PERM_COMMENTS_DELETE_OWN | MSZ_PERM_COMMENTS_DELETE_ANY,
|
||||
'can_delete_any' => MSZ_PERM_COMMENTS_DELETE_ANY,
|
||||
'can_pin' => MSZ_PERM_COMMENTS_PIN,
|
||||
'can_lock' => MSZ_PERM_COMMENTS_LOCK,
|
||||
'can_vote' => MSZ_PERM_COMMENTS_VOTE,
|
||||
]);
|
||||
}
|
||||
|
||||
function comments_pin_status(int $comment, bool $mode): ?string {
|
||||
if($comment < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$status = $mode ? date('Y-m-d H:i:s') : null;
|
||||
|
||||
$setPinStatus = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_comments_posts`
|
||||
SET `comment_pinned` = :status
|
||||
WHERE `comment_id` = :comment
|
||||
AND `comment_reply_to` IS NULL
|
||||
');
|
||||
$setPinStatus->bind('comment', $comment);
|
||||
$setPinStatus->bind('status', $status);
|
||||
|
||||
return $setPinStatus->execute() ? $status : null;
|
||||
}
|
||||
|
||||
function comments_vote_add(int $comment, int $user, int $vote = MSZ_COMMENTS_VOTE_INDIFFERENT): bool {
|
||||
if(!comments_vote_type_valid($vote)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$setVote = \Misuzu\DB::prepare('
|
||||
REPLACE INTO `msz_comments_votes`
|
||||
(`comment_id`, `user_id`, `comment_vote`)
|
||||
VALUES
|
||||
(:comment, :user, :vote)
|
||||
');
|
||||
$setVote->bind('comment', $comment);
|
||||
$setVote->bind('user', $user);
|
||||
$setVote->bind('vote', $vote);
|
||||
return $setVote->execute();
|
||||
}
|
||||
|
||||
function comments_votes_get(int $commentId): array {
|
||||
$getVotes = \Misuzu\DB::prepare(sprintf(
|
||||
'
|
||||
SELECT :id as `id`,
|
||||
(
|
||||
SELECT COUNT(`user_id`)
|
||||
FROM `msz_comments_votes`
|
||||
WHERE `comment_id` = `id`
|
||||
AND `comment_vote` = %1$d
|
||||
) as `likes`,
|
||||
(
|
||||
SELECT COUNT(`user_id`)
|
||||
FROM `msz_comments_votes`
|
||||
WHERE `comment_id` = `id`
|
||||
AND `comment_vote` = %2$d
|
||||
) as `dislikes`
|
||||
',
|
||||
MSZ_COMMENTS_VOTE_LIKE,
|
||||
MSZ_COMMENTS_VOTE_DISLIKE
|
||||
));
|
||||
$getVotes->bind('id', $commentId);
|
||||
return $getVotes->fetch();
|
||||
}
|
||||
|
||||
function comments_category_create(string $name): array {
|
||||
$create = \Misuzu\DB::prepare('
|
||||
INSERT INTO `msz_comments_categories`
|
||||
(`category_name`)
|
||||
VALUES
|
||||
(LOWER(:name))
|
||||
');
|
||||
$create->bind('name', $name);
|
||||
return $create->execute()
|
||||
? comments_category_info(\Misuzu\DB::lastId(), false)
|
||||
: [];
|
||||
}
|
||||
|
||||
function comments_category_lock(int $category, bool $lock): void {
|
||||
$setLock = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_comments_categories`
|
||||
SET `category_locked` = IF(:lock, NOW(), NULL)
|
||||
WHERE `category_id` = :category
|
||||
');
|
||||
$setLock->bind('category', $category);
|
||||
$setLock->bind('lock', $lock);
|
||||
$setLock->execute();
|
||||
}
|
||||
|
||||
define('MSZ_COMMENTS_CATEGORY_INFO_QUERY', '
|
||||
SELECT
|
||||
`category_id`, `category_locked`
|
||||
FROM `msz_comments_categories`
|
||||
WHERE `%s` = %s
|
||||
');
|
||||
define('MSZ_COMMENTS_CATEGORY_INFO_ID', sprintf(
|
||||
MSZ_COMMENTS_CATEGORY_INFO_QUERY,
|
||||
'category_id',
|
||||
':category'
|
||||
));
|
||||
define('MSZ_COMMENTS_CATEGORY_INFO_NAME', sprintf(
|
||||
MSZ_COMMENTS_CATEGORY_INFO_QUERY,
|
||||
'category_name',
|
||||
'LOWER(:category)'
|
||||
));
|
||||
|
||||
function comments_category_info($category, bool $createIfNone = false): array {
|
||||
if(is_int($category)) {
|
||||
$getCategory = \Misuzu\DB::prepare(MSZ_COMMENTS_CATEGORY_INFO_ID);
|
||||
$createIfNone = false;
|
||||
} elseif(is_string($category)) {
|
||||
$getCategory = \Misuzu\DB::prepare(MSZ_COMMENTS_CATEGORY_INFO_NAME);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
$getCategory->bind('category', $category);
|
||||
$categoryInfo = $getCategory->fetch();
|
||||
return $categoryInfo
|
||||
? $categoryInfo
|
||||
: (
|
||||
$createIfNone
|
||||
? comments_category_create($category)
|
||||
: []
|
||||
);
|
||||
}
|
||||
|
||||
define('MSZ_COMMENTS_CATEGORY_QUERY', sprintf(
|
||||
'
|
||||
SELECT
|
||||
p.`comment_id`, p.`comment_text`, p.`comment_reply_to`,
|
||||
p.`comment_created`, p.`comment_pinned`, p.`comment_deleted`,
|
||||
u.`user_id`, u.`username`,
|
||||
COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour`,
|
||||
(
|
||||
SELECT COUNT(`comment_id`)
|
||||
FROM `msz_comments_votes`
|
||||
WHERE `comment_id` = p.`comment_id`
|
||||
AND `comment_vote` = %1$d
|
||||
) AS `comment_likes`,
|
||||
(
|
||||
SELECT COUNT(`comment_id`)
|
||||
FROM `msz_comments_votes`
|
||||
WHERE `comment_id` = p.`comment_id`
|
||||
AND `comment_vote` = %2$d
|
||||
) AS `comment_dislikes`,
|
||||
(
|
||||
SELECT `comment_vote`
|
||||
FROM `msz_comments_votes`
|
||||
WHERE `comment_id` = p.`comment_id`
|
||||
AND `user_id` = :user
|
||||
) AS `comment_user_vote`
|
||||
FROM `msz_comments_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 p.`category_id` = :category
|
||||
%%1$s
|
||||
ORDER BY p.`comment_deleted` ASC, p.`comment_pinned` DESC, p.`comment_id` %%2$s
|
||||
',
|
||||
MSZ_COMMENTS_VOTE_LIKE,
|
||||
MSZ_COMMENTS_VOTE_DISLIKE
|
||||
));
|
||||
|
||||
// The $parent param should never be used outside of this function itself and should always remain the last of the list.
|
||||
function comments_category_get(int $category, int $user, ?int $parent = null): array {
|
||||
$isParent = $parent === null;
|
||||
$getComments = \Misuzu\DB::prepare(sprintf(
|
||||
MSZ_COMMENTS_CATEGORY_QUERY,
|
||||
$isParent ? 'AND p.`comment_reply_to` IS NULL' : 'AND p.`comment_reply_to` = :parent',
|
||||
$isParent ? 'DESC' : 'ASC'
|
||||
));
|
||||
|
||||
if(!$isParent) {
|
||||
$getComments->bind('parent', $parent);
|
||||
}
|
||||
|
||||
$getComments->bind('user', $user);
|
||||
$getComments->bind('category', $category);
|
||||
$comments = $getComments->fetchAll();
|
||||
|
||||
$commentsCount = count($comments);
|
||||
for($i = 0; $i < $commentsCount; $i++) {
|
||||
$comments[$i]['comment_html'] = nl2br(comments_parse_for_display(htmlentities($comments[$i]['comment_text'])));
|
||||
$comments[$i]['comment_replies'] = comments_category_get($category, $user, $comments[$i]['comment_id']);
|
||||
}
|
||||
|
||||
return $comments;
|
||||
}
|
||||
|
||||
function comments_post_create(
|
||||
int $user,
|
||||
int $category,
|
||||
string $text,
|
||||
bool $pinned = false,
|
||||
?int $reply = null,
|
||||
bool $parse = true
|
||||
): int {
|
||||
if($parse) {
|
||||
$text = comments_parse_for_store($text);
|
||||
}
|
||||
|
||||
$create = \Misuzu\DB::prepare('
|
||||
INSERT INTO `msz_comments_posts`
|
||||
(`user_id`, `category_id`, `comment_text`, `comment_pinned`, `comment_reply_to`)
|
||||
VALUES
|
||||
(:user, :category, :text, IF(:pin, NOW(), NULL), :reply)
|
||||
');
|
||||
$create->bind('user', $user);
|
||||
$create->bind('category', $category);
|
||||
$create->bind('text', $text);
|
||||
$create->bind('pin', $pinned ? 1 : 0);
|
||||
$create->bind('reply', $reply < 1 ? null : $reply);
|
||||
return $create->execute() ? \Misuzu\DB::lastId() : 0;
|
||||
}
|
||||
|
||||
function comments_post_delete(int $commentId, bool $delete = true): bool {
|
||||
$deleteComment = \Misuzu\DB::prepare('
|
||||
UPDATE `msz_comments_posts`
|
||||
SET `comment_deleted` = IF(:del, NOW(), NULL)
|
||||
WHERE `comment_id` = :id
|
||||
');
|
||||
$deleteComment->bind('id', $commentId);
|
||||
$deleteComment->bind('del', $delete ? 1 : 0);
|
||||
return $deleteComment->execute();
|
||||
}
|
||||
|
||||
function comments_post_get(int $commentId, bool $parse = true): array {
|
||||
$fetch = \Misuzu\DB::prepare('
|
||||
SELECT
|
||||
p.`comment_id`, p.`category_id`, p.`comment_text`,
|
||||
p.`comment_created`, p.`comment_edited`, p.`comment_deleted`,
|
||||
p.`comment_reply_to`, p.`comment_pinned`,
|
||||
u.`user_id`, u.`username`,
|
||||
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
|
||||
FROM `msz_comments_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 `comment_id` = :id
|
||||
');
|
||||
$fetch->bind('id', $commentId);
|
||||
$comment = $fetch->fetch();
|
||||
|
||||
if($comment && $parse) {
|
||||
$comment['comment_html'] = nl2br(comments_parse_for_display(htmlentities($comment['comment_text'])));
|
||||
}
|
||||
|
||||
return $comment;
|
||||
}
|
||||
|
||||
function comments_post_exists(int $commentId): bool {
|
||||
$fetch = \Misuzu\DB::prepare('
|
||||
SELECT COUNT(`comment_id`) > 0
|
||||
FROM `msz_comments_posts`
|
||||
WHERE `comment_id` = :id
|
||||
');
|
||||
$fetch->bind('id', $commentId);
|
||||
return (bool)$fetch->fetchColumn();
|
||||
}
|
||||
|
||||
function comments_post_replies(int $commentId): array {
|
||||
$getComments = \Misuzu\DB::prepare('
|
||||
SELECT
|
||||
p.`comment_id`, p.`category_id`, p.`comment_text`,
|
||||
p.`comment_created`, p.`comment_edited`, p.`comment_deleted`,
|
||||
p.`comment_reply_to`, p.`comment_pinned`,
|
||||
u.`user_id`, u.`username`,
|
||||
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
|
||||
FROM `msz_comments_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 `comment_reply_to` = :id
|
||||
');
|
||||
$getComments->bind('id', $commentId);
|
||||
return $getComments->fetchAll();
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
{% macro comments_input(category, user, perms, reply_to) %}
|
||||
{% macro comments_input(category, user, reply_to) %}
|
||||
{% set reply_mode = reply_to is not null %}
|
||||
|
||||
{% from 'macros.twig' import avatar %}
|
||||
|
@ -6,17 +6,17 @@
|
|||
|
||||
<form class="comment comment--input{% if reply_mode %} comment--reply{% endif %}"
|
||||
method="post" action="{{ url('comment-create') }}"
|
||||
id="comment-{{ reply_mode ? 'reply-' ~ reply_to.comment_id : 'create-' ~ category.category_id }}">
|
||||
{{ input_hidden('comment[category]', category.category_id) }}
|
||||
id="comment-{{ reply_mode ? 'reply-' ~ reply_to.id : 'create-' ~ category.id }}">
|
||||
{{ input_hidden('comment[category]', category.id) }}
|
||||
{{ input_csrf() }}
|
||||
|
||||
{% if reply_mode %}
|
||||
{{ input_hidden('comment[reply]', reply_to.comment_id) }}
|
||||
{{ input_hidden('comment[reply]', reply_to.id) }}
|
||||
{% endif %}
|
||||
|
||||
<div class="comment__container">
|
||||
<div class="avatar comment__avatar">
|
||||
{{ avatar(user.user_id, reply_mode ? 40 : 50, user.username) }}
|
||||
{{ avatar(user.id, reply_mode ? 40 : 50, user.username) }}
|
||||
</div>
|
||||
<div class="comment__content">
|
||||
<textarea
|
||||
|
@ -24,10 +24,10 @@
|
|||
name="comment[text]" placeholder="Share your extensive insights..."></textarea>
|
||||
<div class="comment__actions">
|
||||
{% if not reply_mode %}
|
||||
{% if perms.can_pin %}
|
||||
{% if user.commentPerms.can_pin|default(false) %}
|
||||
{{ input_checkbox('comment[pin]', 'Pin this comment', false, 'comment__action') }}
|
||||
{% endif %}
|
||||
{% if perms.can_lock %}
|
||||
{% if user.commentPerms.can_lock|default(false) %}
|
||||
{{ input_checkbox('comment[lock]', 'Toggle locked status', false, 'comment__action') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -40,110 +40,101 @@
|
|||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro comments_entry(comment, indent, category, user, perms) %}
|
||||
{% macro comments_entry(comment, indent, category, user) %}
|
||||
{% from 'macros.twig' import avatar %}
|
||||
{% from '_layout/input.twig' import input_checkbox_raw %}
|
||||
{% set is_deleted = comment.comment_deleted is not null %}
|
||||
{% set hide_details = is_deleted and not perms.can_delete_any %}
|
||||
{% set hide_details = comment.userId < 1 or comment.deleted and not user.commentPerms.can_delete_any|default(false) %}
|
||||
|
||||
{% if perms.can_delete_any or (not is_deleted or comment.comment_replies|length > 0) %}
|
||||
{% set is_pinned = comment.comment_pinned is not null %}
|
||||
|
||||
<div class="comment{% if is_deleted %} comment--deleted{% endif %}" id="comment-{{ comment.comment_id }}">
|
||||
{% if user.commentPerms.can_delete_any|default(false) or (not comment.deleted or comment.replies(user)|length > 0) %}
|
||||
<div class="comment{% if comment.deleted %} comment--deleted{% endif %}" id="comment-{{ comment.id }}">
|
||||
<div class="comment__container">
|
||||
{% if hide_details %}
|
||||
<div class="comment__avatar">
|
||||
{{ avatar(0, indent > 1 ? 40 : 50) }}
|
||||
</div>
|
||||
{% else %}
|
||||
<a class="comment__avatar" href="{{ url('user-profile', {'user':comment.user_id}) }}">
|
||||
{{ avatar(comment.user_id, indent > 1 ? 40 : 50, comment.username) }}
|
||||
<a class="comment__avatar" href="{{ url('user-profile', {'user':comment.user.id}) }}">
|
||||
{{ avatar(comment.user.id, indent > 1 ? 40 : 50, comment.user.username) }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="comment__content">
|
||||
<div class="comment__info">
|
||||
{% if not hide_details %}
|
||||
<a class="comment__user comment__user--link"
|
||||
href="{{ url('user-profile', {'user':comment.user_id}) }}"
|
||||
style="{{ comment.user_colour|html_colour }}">{{ comment.username }}</a>
|
||||
href="{{ url('user-profile', {'user':comment.user.id}) }}"
|
||||
style="--user-colour: {{ comment.user.colour}}">{{ comment.user.username }}</a>
|
||||
{% endif %}
|
||||
<a class="comment__link" href="#comment-{{ comment.comment_id }}">
|
||||
<a class="comment__link" href="#comment-{{ comment.id }}">
|
||||
<time class="comment__date"
|
||||
title="{{ comment.comment_created|date('r') }}"
|
||||
datetime="{{ comment.comment_created|date('c') }}">
|
||||
{{ comment.comment_created|time_diff }}
|
||||
title="{{ comment.createdTime|date('r') }}"
|
||||
datetime="{{ comment.createdTime|date('c') }}">
|
||||
{{ comment.createdTime|time_diff }}
|
||||
</time>
|
||||
</a>
|
||||
{% if is_pinned %}
|
||||
{% if comment.pinned %}
|
||||
<span class="comment__pin">{% apply spaceless %}
|
||||
Pinned
|
||||
{% if comment.comment_pinned != comment.comment_created %}
|
||||
<time title="{{ comment.comment_pinned|date('r') }}"
|
||||
datetime="{{ comment.comment_pinned|date('c') }}">
|
||||
{{ comment.comment_pinned|time_diff }}
|
||||
{% if comment.pinnedTime != comment.createdTime %}
|
||||
<time title="{{ comment.pinnedTime|date('r') }}"
|
||||
datetime="{{ comment.pinnedTime|date('c') }}">
|
||||
{{ comment.pinnedTime|time_diff }}
|
||||
</time>
|
||||
{% endif %}
|
||||
{% endapply %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="comment__text">
|
||||
{{ hide_details ? '(deleted)' : (comment.comment_html is defined ? comment.comment_html|raw : comment.comment_text|nl2br) }}
|
||||
{{ hide_details ? '(deleted)' : comment.parsedText|raw }}
|
||||
</div>
|
||||
<div class="comment__actions">
|
||||
{% if not is_deleted and user is not null %}
|
||||
{% if perms.can_vote %}
|
||||
{% set like_vote_state = comment.comment_user_vote == constant('MSZ_COMMENTS_VOTE_LIKE')
|
||||
? constant('MSZ_COMMENTS_VOTE_INDIFFERENT')
|
||||
: constant('MSZ_COMMENTS_VOTE_LIKE') %}
|
||||
{% set dislike_vote_state = comment.comment_user_vote == constant('MSZ_COMMENTS_VOTE_DISLIKE')
|
||||
? constant('MSZ_COMMENTS_VOTE_INDIFFERENT')
|
||||
: constant('MSZ_COMMENTS_VOTE_DISLIKE') %}
|
||||
{% if not comment.deleted and user is not null %}
|
||||
{% if user.commentPerms.can_vote|default(false) %}
|
||||
{% set like_vote_state = comment.userVote > 0 ? 0 : 1 %}
|
||||
{% set dislike_vote_state = comment.userVote < 0 ? 0 : -1 %}
|
||||
|
||||
<a class="comment__action comment__action--link comment__action--vote comment__action--like{% if comment.comment_user_vote == constant('MSZ_COMMENTS_VOTE_LIKE') %} comment__action--voted{% endif %}" data-comment-id="{{ comment.comment_id }}" data-comment-vote="{{ like_vote_state }}"
|
||||
href="{{ url('comment-vote', {'comment':comment.comment_id,'vote':like_vote_state}) }}">
|
||||
<!--i class="fas fa-thumbs-up"></i-->
|
||||
<a class="comment__action comment__action--link comment__action--vote comment__action--like{% if comment.userVote > 0 %} comment__action--voted{% endif %}" data-comment-id="{{ comment.id }}" data-comment-vote="{{ like_vote_state }}"
|
||||
href="{{ url('comment-vote', {'comment':comment.id,'vote':like_vote_state}) }}">
|
||||
Like
|
||||
{% if comment.comment_likes > 0 %}
|
||||
({{ comment.comment_likes|number_format }})
|
||||
{% if comment.likes > 0 %}
|
||||
({{ comment.likes|number_format }})
|
||||
{% endif %}
|
||||
</a>
|
||||
<a class="comment__action comment__action--link comment__action--vote comment__action--dislike{% if comment.comment_user_vote == constant('MSZ_COMMENTS_VOTE_DISLIKE') %} comment__action--voted{% endif %}" data-comment-id="{{ comment.comment_id }}" data-comment-vote="{{ dislike_vote_state }}"
|
||||
href="{{ url('comment-vote', {'comment':comment.comment_id,'vote':dislike_vote_state}) }}">
|
||||
<!--i class="fas fa-thumbs-down"></i-->
|
||||
<a class="comment__action comment__action--link comment__action--vote comment__action--dislike{% if comment.userVote < 0 %} comment__action--voted{% endif %}" data-comment-id="{{ comment.id }}" data-comment-vote="{{ dislike_vote_state }}"
|
||||
href="{{ url('comment-vote', {'comment':comment.id,'vote':dislike_vote_state}) }}">
|
||||
Dislike
|
||||
{% if comment.comment_dislikes > 0 %}
|
||||
({{ comment.comment_dislikes|number_format }})
|
||||
{% if comment.dislikes > 0 %}
|
||||
({{ comment.dislikes|number_format }})
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.can_comment %}
|
||||
<label class="comment__action comment__action--link" for="comment-reply-toggle-{{ comment.comment_id }}">Reply</label>
|
||||
{% if user.commentPerms.can_comment|default(false) %}
|
||||
<label class="comment__action comment__action--link" for="comment-reply-toggle-{{ comment.id }}">Reply</label>
|
||||
{% endif %}
|
||||
{% if perms.can_delete_any or (comment.user_id == user.user_id and perms.can_delete) %}
|
||||
<a class="comment__action comment__action--link comment__action--hide comment__action--delete" data-comment-id="{{ comment.comment_id }}" href="{{ url('comment-delete', {'comment':comment.comment_id}) }}">Delete</a>
|
||||
{% if user.commentPerms.can_delete_any|default(false) or (comment.user.id == user.id and user.commentPerms.can_delete|default(false)) %}
|
||||
<a class="comment__action comment__action--link comment__action--hide comment__action--delete" data-comment-id="{{ comment.id }}" href="{{ url('comment-delete', {'comment':comment.id}) }}">Delete</a>
|
||||
{% endif %}
|
||||
{# if user is not null %}
|
||||
<a class="comment__action comment__action--link comment__action--hide" href="#">Report</a>
|
||||
{% endif #}
|
||||
{% if comment.comment_reply_to is null and perms.can_pin %}
|
||||
<a class="comment__action comment__action--link comment__action--hide comment__action--pin" data-comment-id="{{ comment.comment_id }}" data-comment-pinned="{{ is_pinned ? '1' : '0' }}" href="{{ url('comment-' ~ (is_pinned ? 'unpin' : 'pin'), {'comment':comment.comment_id}) }}">{{ is_pinned ? 'Unpin' : 'Pin' }}</a>
|
||||
{% if not comment.hasParent and user.commentPerms.can_pin|default(false) %}
|
||||
<a class="comment__action comment__action--link comment__action--hide comment__action--pin" data-comment-id="{{ comment.id }}" data-comment-pinned="{{ comment.pinned ? '1' : '0' }}" href="{{ url('comment-' ~ (comment.pinned ? 'unpin' : 'pin'), {'comment':comment.id}) }}">{{ comment.pinned ? 'Unpin' : 'Pin' }}</a>
|
||||
{% endif %}
|
||||
{% elseif perms.can_delete_any %}
|
||||
<a class="comment__action comment__action--link comment__action--restore" data-comment-id="{{ comment.comment_id }}" href="{{ url('comment-restore', {'comment':comment.comment_id}) }}">Restore</a>
|
||||
{% elseif user.commentPerms.can_delete_any|default(false) %}
|
||||
<a class="comment__action comment__action--link comment__action--restore" data-comment-id="{{ comment.id }}" href="{{ url('comment-restore', {'comment':comment.id}) }}">Restore</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comment__replies comment__replies--indent-{{ indent }}" id="comment-{{ comment.comment_id }}-replies">
|
||||
<div class="comment__replies comment__replies--indent-{{ indent }}" id="comment-{{ comment.id }}-replies">
|
||||
{% from _self import comments_entry, comments_input %}
|
||||
{% if user|default(null) is not null and category|default(null) is not null and perms|default(null) is not null and perms.can_comment %}
|
||||
{{ input_checkbox_raw('', false, 'comment__reply-toggle', '', false, {'id':'comment-reply-toggle-' ~ comment.comment_id}) }}
|
||||
{{ comments_input(category, user, perms, comment) }}
|
||||
{% if user|default(null) is not null and category|default(null) is not null and user.commentPerms.can_comment|default(false) %}
|
||||
{{ input_checkbox_raw('', false, 'comment__reply-toggle', '', false, {'id':'comment-reply-toggle-' ~ comment.id}) }}
|
||||
{{ comments_input(category, user, comment) }}
|
||||
{% endif %}
|
||||
{% if comment.comment_replies is defined and comment.comment_replies|length > 0 %}
|
||||
{% for reply in comment.comment_replies %}
|
||||
{{ comments_entry(reply, indent + 1, category, user, perms) }}
|
||||
{% if comment.replies|length > 0 %}
|
||||
{% for reply in comment.replies %}
|
||||
{{ comments_entry(reply, indent + 1, category, user) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -151,34 +142,34 @@
|
|||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro comments_section(comments, category, user, perms) %}
|
||||
{% macro comments_section(category, user) %}
|
||||
<div class="comments" id="comments">
|
||||
<div class="comments__input">
|
||||
{% if user|default(null) is null %}
|
||||
<div class="comments__notice">
|
||||
Please <a href="{{ url('auth-login') }}" class="comments__notice__link">login</a> to comment.
|
||||
</div>
|
||||
{% elseif category|default(null) is null or perms|default(null) is null %}
|
||||
{% elseif category|default(null) is null %}
|
||||
<div class="comments__notice">
|
||||
Posting new comments here is disabled.
|
||||
</div>
|
||||
{% elseif not perms.can_lock and category.category_locked is not null %}
|
||||
{% elseif not user.commentPerms.can_lock|default(false) and category.locked %}
|
||||
<div class="comments__notice">
|
||||
This comment section was locked, <time datetime="{{ category.category_locked|date('c') }}" title="{{ category.category_locked|date('r') }}">{{ category.category_locked|time_diff }}</time>.
|
||||
This comment section was locked, <time datetime="{{ category.lockedTime|date('c') }}" title="{{ category.lockedTime|date('r') }}">{{ category.lockedTime|time_diff }}</time>.
|
||||
</div>
|
||||
{% elseif not perms.can_comment %}
|
||||
{% elseif not user.commentPerms.can_comment|default(false) %}
|
||||
<div class="comments__notice">
|
||||
You are not allowed to post comments.
|
||||
</div>
|
||||
{% else %}
|
||||
{% from _self import comments_input %}
|
||||
{{ comments_input(category, user, perms) }}
|
||||
{{ comments_input(category, user) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if perms.can_lock and category.category_locked is not null %}
|
||||
{% if user.commentPerms.can_lock|default(false) and category.locked %}
|
||||
<div class="comments__notice comments__notice--staff">
|
||||
This comment section was locked, <time datetime="{{ category.category_locked|date('c') }}" title="{{ category.category_locked|date('r') }}">{{ category.category_locked|time_diff }}</time>.
|
||||
This comment section was locked, <time datetime="{{ category.lockedTime|date('c') }}" title="{{ category.lockedTime|date('r') }}">{{ category.lockedTime|time_diff }}</time>.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -189,13 +180,13 @@
|
|||
</noscript>
|
||||
|
||||
<div class="comments__listing">
|
||||
{% if comments|length > 0 %}
|
||||
{% if category.posts|length > 0 %}
|
||||
{% from _self import comments_entry %}
|
||||
{% for comment in comments %}
|
||||
{{ comments_entry(comment, 1, category, user, perms) }}
|
||||
{% for comment in category.posts(user) %}
|
||||
{{ comments_entry(comment, 1, category, user) }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="comments__none" id="_no_comments_notice_{{ category.category_id }}">
|
||||
<div class="comments__none" id="_no_comments_notice_{{ category.id }}">
|
||||
There are no comments yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -83,10 +83,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if comments is defined %}
|
||||
{% if comments_category is defined %}
|
||||
<div class="container">
|
||||
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments for ' ~ change.change_date) }}
|
||||
{{ comments_section(comments, comments_category, current_user|default(null), comments_perms) }}
|
||||
{{ comments_section(comments_category, comments_user) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -34,10 +34,10 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if comments is defined %}
|
||||
{% if comments_category is defined %}
|
||||
<div class="container">
|
||||
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments') }}
|
||||
{{ comments_section(comments, comments_category, current_user|default(null), comments_perms) }}
|
||||
{{ comments_section(comments_category, comments_user) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<div class="news__preview__links">
|
||||
<a href="{{ url('news-post', {'post': post.id}) }}" class="news__preview__link">Continue reading</a>
|
||||
<a href="{{ url('news-post-comments', {'post': post.id}) }}" class="news__preview__link">
|
||||
{{ post.commentCount < 1 ? 'No' : post.commentCount|number_format }} comment{{ post.commentCount != 1 ? 's' : '' }}
|
||||
{{ not post.hasCommentSection or post.commentSection.postCount < 1 ? 'No' : post.commentSection.postCount|number_format }} comment{{ not post.hasCommentSection or post.commentSection.postCount != 1 ? 's' : '' }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,10 +10,10 @@
|
|||
{% block content %}
|
||||
{{ news_post(post_info) }}
|
||||
|
||||
{% if comments is defined %}
|
||||
{% if comments_info is defined %}
|
||||
<div class="container">
|
||||
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments') }}
|
||||
{{ comments_section(comments, comments_category, current_user|default(null), comments_perms) }}
|
||||
{{ comments_section(comments_info, comments_user) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% from '_layout/input.twig' import input_hidden, input_csrf, input_text, input_checkbox, input_file, input_file_raw, input_select %}
|
||||
|
||||
{% if profile_user is defined %}
|
||||
{% set canonical_url = url('user-profile', {'user': profile_user.user_id}) %}
|
||||
{% set canonical_url = url('user-profile', {'user': profile_user.id}) %}
|
||||
{% set title = profile_user.username %}
|
||||
{% else %}
|
||||
{% set title = 'User not found!' %}
|
||||
|
@ -12,7 +12,7 @@
|
|||
|
||||
{% block content %}
|
||||
{% if profile_is_editing %}
|
||||
<form class="profile" method="post" action="{{ url('user-profile', {'user': profile_user.user_id}) }}" enctype="multipart/form-data">
|
||||
<form class="profile" method="post" action="{{ url('user-profile', {'user': profile_user.id}) }}" enctype="multipart/form-data">
|
||||
{{ input_csrf('profile') }}
|
||||
|
||||
{% if perms.edit_avatar %}
|
||||
|
@ -20,7 +20,7 @@
|
|||
|
||||
<script>
|
||||
function updateAvatarPreview(name, url, preview) {
|
||||
url = url || "{{ url('user-avatar', {'user': profile_user.user_id, 'res': 240})|raw }}";
|
||||
url = url || "{{ url('user-avatar', {'user': profile_user.id, 'res': 240})|raw }}";
|
||||
preview = preview || document.getElementById('avatar-preview');
|
||||
preview.src = url;
|
||||
preview.title = name;
|
||||
|
@ -211,7 +211,7 @@
|
|||
|
||||
{% if profile_warnings|length > 0 or profile_warnings_can_manage %}
|
||||
<div class="container profile__container profile__warning__container" id="account-standing">
|
||||
{{ container_title('Account Standing', false, profile_warnings_can_manage ? url('manage-users-warnings', {'user': profile_user.user_id}) : '') }}
|
||||
{{ container_title('Account Standing', false, profile_warnings_can_manage ? url('manage-users-warnings', {'user': profile_user.id}) : '') }}
|
||||
|
||||
<div class="profile__warning">
|
||||
<div class="profile__warning__background"></div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends 'master.twig' %}
|
||||
|
||||
{% if profile_user is defined %}
|
||||
{% set image = url('user-avatar', {'user': profile_user.user_id, 'res': 200}) %}
|
||||
{% set manage_link = url('manage-user', {'user': profile_user.user_id}) %}
|
||||
{% set image = url('user-avatar', {'user': profile_user.id, 'res': 200}) %}
|
||||
{% set manage_link = url('manage-user', {'user': profile_user.id}) %}
|
||||
{% set stats = [
|
||||
{
|
||||
'title': 'Joined',
|
||||
|
@ -17,25 +17,25 @@
|
|||
{
|
||||
'title': 'Following',
|
||||
'value': profile_stats.following_count,
|
||||
'url': url('user-profile-following', {'user': profile_user.user_id}),
|
||||
'url': url('user-profile-following', {'user': profile_user.id}),
|
||||
'active': profile_mode == 'following',
|
||||
},
|
||||
{
|
||||
'title': 'Followers',
|
||||
'value': profile_stats.followers_count,
|
||||
'url': url('user-profile-followers', {'user': profile_user.user_id}),
|
||||
'url': url('user-profile-followers', {'user': profile_user.id}),
|
||||
'active': profile_mode == 'followers',
|
||||
},
|
||||
{
|
||||
'title': 'Topics',
|
||||
'value': profile_stats.forum_topic_count,
|
||||
'url': url('user-profile-forum-topics', {'user': profile_user.user_id}),
|
||||
'url': url('user-profile-forum-topics', {'user': profile_user.id}),
|
||||
'active': profile_mode == 'forum-topics',
|
||||
},
|
||||
{
|
||||
'title': 'Posts',
|
||||
'value': profile_stats.forum_post_count,
|
||||
'url': url('user-profile-forum-posts', {'user': profile_user.user_id}),
|
||||
'url': url('user-profile-forum-posts', {'user': profile_user.id}),
|
||||
'active': profile_mode == 'forum-posts',
|
||||
},
|
||||
{
|
||||
|
|
75
utility.php
75
utility.php
|
@ -1,32 +1,32 @@
|
|||
<?php
|
||||
function array_test(array $array, callable $func): bool {
|
||||
foreach($array as $value) {
|
||||
if(!$func($value)) {
|
||||
foreach($array as $value)
|
||||
if(!$func($value))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function array_apply(array $array, callable $func): array {
|
||||
for($i = 0; $i < count($array); $i++) {
|
||||
for($i = 0; $i < count($array); ++$i)
|
||||
$array[$i] = $func($array[$i]);
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
function array_bit_or(array $array1, array $array2): array {
|
||||
foreach($array1 as $key => $value) {
|
||||
foreach($array1 as $key => $value)
|
||||
$array1[$key] |= $array2[$key] ?? 0;
|
||||
}
|
||||
|
||||
return $array1;
|
||||
}
|
||||
|
||||
function array_rand_value(array $array) {
|
||||
return $array[array_rand($array)];
|
||||
return $array[mt_rand(0, count($array) - 1)];
|
||||
}
|
||||
|
||||
function array_find(array $array, callable $callback) {
|
||||
foreach($array as $item)
|
||||
if($callback($item))
|
||||
return $item;
|
||||
return null;
|
||||
}
|
||||
|
||||
function clamp($num, int $min, int $max): int {
|
||||
|
@ -76,72 +76,35 @@ function unique_chars(string $input, bool $multibyte = true): int {
|
|||
}
|
||||
|
||||
function byte_symbol(int $bytes, bool $decimal = false, array $symbols = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']): string {
|
||||
if($bytes < 1) {
|
||||
if($bytes < 1)
|
||||
return '0 B';
|
||||
}
|
||||
|
||||
$divider = $decimal ? 1000 : 1024;
|
||||
$exp = floor(log($bytes) / log($divider));
|
||||
$bytes = $bytes / pow($divider, floor($exp));
|
||||
$bytes = $bytes / pow($divider, $exp);
|
||||
$symbol = $symbols[$exp];
|
||||
|
||||
return sprintf("%.2f %s%sB", $bytes, $symbol, $symbol !== '' && !$decimal ? 'i' : '');
|
||||
}
|
||||
|
||||
// For chat emote list, nuke this when Sharp Chat comms are in this project
|
||||
function emotes_list(int $hierarchy = PHP_INT_MAX, bool $unique = false, bool $order = true): array {
|
||||
$getEmotes = \Misuzu\DB::prepare('
|
||||
SELECT e.`emote_id`, e.`emote_order`, e.`emote_hierarchy`, e.`emote_url`,
|
||||
s.`emote_string_order`, s.`emote_string`
|
||||
FROM `msz_emoticons_strings` AS s
|
||||
LEFT JOIN `msz_emoticons` AS e
|
||||
ON e.`emote_id` = s.`emote_id`
|
||||
WHERE `emote_hierarchy` <= :hierarchy
|
||||
ORDER BY IF(:order, e.`emote_order`, e.`emote_id`), s.`emote_string_order`
|
||||
');
|
||||
$getEmotes->bind('hierarchy', $hierarchy);
|
||||
$getEmotes->bind('order', $order);
|
||||
$emotes = $getEmotes->fetchAll();
|
||||
|
||||
// Removes aliases, emote with lowest ordering is considered the main
|
||||
if($unique) {
|
||||
$existing = [];
|
||||
|
||||
for($i = 0; $i < count($emotes); $i++) {
|
||||
if(in_array($emotes[$i]['emote_url'], $existing)) {
|
||||
unset($emotes[$i]);
|
||||
} else {
|
||||
$existing[] = $emotes[$i]['emote_url'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $emotes;
|
||||
}
|
||||
|
||||
function safe_delete(string $path): void {
|
||||
$path = realpath($path);
|
||||
|
||||
if(empty($path)) {
|
||||
if(empty($path))
|
||||
return;
|
||||
}
|
||||
|
||||
if(is_dir($path)) {
|
||||
rmdir($path);
|
||||
return;
|
||||
}
|
||||
|
||||
if(is_file($path)) {
|
||||
if(is_file($path))
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
// mkdir but it fails silently
|
||||
function mkdirs(string $path, bool $recursive = false, int $mode = 0777): bool {
|
||||
if(file_exists($path)) {
|
||||
if(file_exists($path))
|
||||
return true;
|
||||
}
|
||||
|
||||
return mkdir($path, $mode, $recursive);
|
||||
}
|
||||
|
||||
|
@ -270,8 +233,8 @@ 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, 'res' => $resolution * 2]);
|
||||
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'] ?? ''));
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue