Rewrote comments backend.

This commit is contained in:
flash 2020-05-18 21:27:34 +00:00
commit 41f1a59b1d
36 changed files with 1380 additions and 799 deletions

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

View file

@ -30,12 +30,6 @@
.changelog__entry__action__text { .changelog__entry__action__text {
width: 100%; 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 { .changelog__entry__datetime {
min-width: 100px; min-width: 100px;

View file

@ -2,7 +2,7 @@
--action-colour: var(--accent-colour); --action-colour: var(--accent-colour);
border: 1px solid var(--action-colour); border: 1px solid var(--action-colour);
background-color: var(--action-colour); background-color: var(--background-colour);
display: flex; display: flex;
align-items: stretch; align-items: stretch;
flex: 1 0 auto; flex: 1 0 auto;

View file

@ -64,7 +64,6 @@ require_once 'utility.php';
require_once 'src/perms.php'; require_once 'src/perms.php';
require_once 'src/audit_log.php'; require_once 'src/audit_log.php';
require_once 'src/changelog.php'; require_once 'src/changelog.php';
require_once 'src/comments.php';
require_once 'src/manage.php'; require_once 'src/manage.php';
require_once 'src/url.php'; require_once 'src/url.php';
require_once 'src/Forum/perms.php'; require_once 'src/Forum/perms.php';

View file

@ -3,6 +3,7 @@ namespace Misuzu;
use Misuzu\Net\IPAddress; use Misuzu\Net\IPAddress;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
require_once '../../misuzu.php'; require_once '../../misuzu.php';
@ -44,7 +45,6 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
break; break;
} }
$userData = User::findForLogin($_POST['login']['username']);
$attemptsRemainingError = sprintf( $attemptsRemainingError = sprintf(
"%d attempt%s remaining", "%d attempt%s remaining",
$remainingAttempts - 1, $remainingAttempts - 1,
@ -52,7 +52,9 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
); );
$loginFailedError = "Invalid username or password, {$attemptsRemainingError}."; $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); user_login_attempt_record(false, null, $ipAddress, $userAgent);
$notices[] = $loginFailedError; $notices[] = $loginFailedError;
break; break;
@ -64,7 +66,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
} }
if($userData->isDeleted() || !$userData->checkPassword($_POST['login']['password'])) { 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; $notices[] = $loginFailedError;
break; break;
} }
@ -73,31 +75,31 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
$userData->setPassword($_POST['login']['password']); $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."; $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; break;
} }
if($userData->hasTOTP()) { if($userData->hasTOTP()) {
url_redirect('auth-two-factor', [ url_redirect('auth-two-factor', [
'token' => user_auth_tfa_token_create($userData->user_id), 'token' => user_auth_tfa_token_create($userData->getId()),
]); ]);
return; return;
} }
user_login_attempt_record(true, $userData->user_id, $ipAddress, $userAgent); user_login_attempt_record(true, $userData->getId(), $ipAddress, $userAgent);
$sessionKey = user_session_create($userData->user_id, $ipAddress, $userAgent); $sessionKey = user_session_create($userData->getId(), $ipAddress, $userAgent);
if(empty($sessionKey)) { if(empty($sessionKey)) {
$notices[] = "Something broke while creating a session for you, please tell an administrator or developer about this!"; $notices[] = "Something broke while creating a session for you, please tell an administrator or developer about this!";
break; break;
} }
user_session_start($userData->user_id, $sessionKey); user_session_start($userData->getId(), $sessionKey);
$cookieLife = strtotime(user_session_current('session_expires')); $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); setcookie('msz_auth', $cookieValue, $cookieLife, '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
if(!is_local_url($loginRedirect)) { if(!is_local_url($loginRedirect)) {

View file

@ -82,8 +82,8 @@ while(!$restricted && !empty($register)) {
break; break;
} }
user_role_add($createUser->user_id, MSZ_ROLE_MAIN); user_role_add($createUser->getId(), MSZ_ROLE_MAIN);
url_redirect('auth-login-welcome', ['username' => $createUser->username]); url_redirect('auth-login-welcome', ['username' => $createUser->getUsername()]);
return; return;
} }

View file

@ -1,14 +1,17 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
require_once '../misuzu.php'; require_once '../misuzu.php';
$changelogChange = !empty($_GET['c']) && is_string($_GET['c']) ? (int)$_GET['c'] : 0; $changelogChange = !empty($_GET['c']) && is_string($_GET['c']) ? (int)$_GET['c'] : 0;
$changelogDate = !empty($_GET['d']) && is_string($_GET['d']) ? (string)$_GET['d'] : ''; $changelogDate = !empty($_GET['d']) && is_string($_GET['d']) ? (string)$_GET['d'] : '';
$changelogUser = !empty($_GET['u']) && is_string($_GET['u']) ? (int)$_GET['u'] : 0; $changelogUser = !empty($_GET['u']) && is_string($_GET['u']) ? (int)$_GET['u'] : 0;
$changelogTags = !empty($_GET['t']) && is_string($_GET['t']) ? (string)$_GET['t'] : ''; $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)));
if($changelogChange > 0) { if($changelogChange > 0) {
$change = changelog_change_get($changelogChange); $change = changelog_change_get($changelogChange);
@ -18,14 +21,25 @@ if($changelogChange > 0) {
return; 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', [ Template::render('changelog.change', [
'change' => $change, 'change' => $change,
'tags' => changelog_change_tags_get($change['change_id']), 'tags' => changelog_change_tags_get($change['change_id']),
'comments_category' => $commentsCategory = comments_category_info( 'comments_category' => $commentsCategory,
"changelog-date-{$change['change_date']}", 'comments_user' => $commentsUser,
true
),
'comments' => comments_category_get($commentsCategory['category_id'], user_session_current('user_id', 0)),
]); ]);
return; return;
} }
@ -52,9 +66,23 @@ if(!$changes) {
} }
if(!empty($changelogDate) && count($changes) > 0) { 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([ Template::set([
'comments_category' => $commentsCategory = comments_category_info("changelog-date-{$changelogDate}", true), 'comments_category' => $commentsCategory,
'comments' => comments_category_get($commentsCategory['category_id'], user_session_current('user_id', 0)), 'comments_user' => $commentsUser,
]); ]);
} }

View file

@ -1,6 +1,15 @@
<?php <?php
namespace Misuzu; 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'; require_once '../misuzu.php';
// basing whether or not this is an xhr request on whether a referrer header is present // basing whether or not this is an xhr request on whether a referrer header is present
@ -20,28 +29,36 @@ if(!CSRF::validateRequest()) {
return; 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); echo render_info_or_json($isXHR, 'You must be logged in to manage comments.', 401);
return; return;
} }
$currentUserId = user_session_current('user_id', 0); if(user_warning_check_expiration($currentUserInfo->getId(), MSZ_WARN_BAN) > 0) {
if(user_warning_check_expiration($currentUserId, MSZ_WARN_BAN) > 0) {
echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403); echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403);
return; 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); echo render_info_or_json($isXHR, 'You have been silenced, check your profile for more information.', 403);
return; return;
} }
header(CSRF::header()); header(CSRF::header());
$commentPerms = comments_get_perms($currentUserId); $commentPerms = $currentUserInfo->commentPerms();
$commentId = !empty($_GET['c']) && is_string($_GET['c']) ? (int)$_GET['c'] : 0; $commentId = (int)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$commentMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : ''; $commentMode = filter_input(INPUT_GET, 'm');
$commentVote = !empty($_GET['v']) && is_string($_GET['v']) ? (int)$_GET['v'] : MSZ_COMMENTS_VOTE_INDIFFERENT; $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) { switch($commentMode) {
case 'pin': case 'pin':
@ -51,38 +68,37 @@ switch($commentMode) {
break; break;
} }
$commentInfo = comments_post_get($commentId, false); if($commentInfo2->isDeleted()) {
if(!$commentInfo || $commentInfo['comment_deleted'] !== null) {
echo render_info_or_json($isXHR, "This comment doesn't exist!", 400); echo render_info_or_json($isXHR, "This comment doesn't exist!", 400);
break; break;
} }
if($commentInfo['comment_reply_to'] !== null) { if($commentInfo2->hasParent()) {
echo render_info_or_json($isXHR, "You can't pin replies!", 400); echo render_info_or_json($isXHR, "You can't pin replies!", 400);
break; break;
} }
$isPinning = $commentMode === 'pin'; $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); echo render_info_or_json($isXHR, 'This comment is already pinned.', 400);
break; break;
} elseif(!$isPinning && empty($commentInfo['comment_pinned'])) { } elseif(!$isPinning && !$commentInfo2->isPinned()) {
echo render_info_or_json($isXHR, "This comment isn't pinned yet.", 400); echo render_info_or_json($isXHR, "This comment isn't pinned yet.", 400);
break; break;
} }
$commentPinned = comments_pin_status($commentInfo['comment_id'], $isPinning); $commentInfo2->setPinned($isPinning);
$commentInfo2->save();
if(!$isXHR) { if(!$isXHR) {
redirect($redirect . '#comment-' . $commentInfo['comment_id']); redirect($redirect . '#comment-' . $commentInfo2->getId());
break; break;
} }
echo json_encode([ echo json_encode([
'comment_id' => $commentInfo['comment_id'], 'comment_id' => $commentInfo2->getId(),
'comment_pinned' => $commentPinned, 'comment_pinned' => ($time = $commentInfo2->getPinnedTime()) < 0 ? null : date('Y-m-d H:i:s', $time),
]); ]);
break; break;
@ -92,30 +108,24 @@ switch($commentMode) {
break; break;
} }
if(!comments_vote_type_valid($commentVote)) { if($commentInfo2->isDeleted()) {
echo render_info_or_json($isXHR, 'Invalid vote action.', 400);
break;
}
$commentInfo = comments_post_get($commentId, false);
if(!$commentInfo || $commentInfo['comment_deleted'] !== null) {
echo render_info_or_json($isXHR, "This comment doesn't exist!", 400); echo render_info_or_json($isXHR, "This comment doesn't exist!", 400);
break; break;
} }
$voteResult = comments_vote_add( if($commentVote > 0)
$commentInfo['comment_id'], $commentInfo2->addPositiveVote($currentUserInfo);
user_session_current('user_id', 0), elseif($commentVote < 0)
$commentVote $commentInfo2->addNegativeVote($currentUserInfo);
); else
$commentInfo2->removeVote($currentUserInfo);
if(!$isXHR) { if(!$isXHR) {
redirect($redirect . '#comment-' . $commentInfo['comment_id']); redirect($redirect . '#comment-' . $commentInfo2->getId());
break; break;
} }
echo json_encode(comments_votes_get($commentInfo['comment_id'])); echo json_encode($commentInfo2->votes());
break; break;
case 'delete': case 'delete':
@ -124,17 +134,7 @@ switch($commentMode) {
break; break;
} }
$commentInfo = comments_post_get($commentId, false); if($commentInfo2->isDeleted()) {
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) {
echo render_info_or_json( echo render_info_or_json(
$isXHR, $isXHR,
$commentPerms['can_delete_any'] ? 'This comment is already marked for deletion.' : "This comment doesn't exist.", $commentPerms['can_delete_any'] ? 'This comment is already marked for deletion.' : "This comment doesn't exist.",
@ -143,24 +143,25 @@ switch($commentMode) {
break; break;
} }
$isOwnComment = $commentInfo2->getUserId() === $currentUserInfo->getId();
$isModAction = $commentPerms['can_delete_any'] && !$isOwnComment;
if(!$isModAction && !$isOwnComment) { if(!$isModAction && !$isOwnComment) {
echo render_info_or_json($isXHR, "You're not allowed to delete comments made by others.", 403); echo render_info_or_json($isXHR, "You're not allowed to delete comments made by others.", 403);
break; break;
} }
if(!comments_post_delete($commentInfo['comment_id'])) { $commentInfo2->setDeleted(true);
echo render_info_or_json($isXHR, 'Failed to delete comment.', 500); $commentInfo2->save();
break;
}
if($isModAction) { if($isModAction) {
audit_log(MSZ_AUDIT_COMMENT_ENTRY_DELETE_MOD, $currentUserId, [ audit_log(MSZ_AUDIT_COMMENT_ENTRY_DELETE_MOD, $currentUserInfo->getId(), [
$commentInfo['comment_id'], $commentInfo2->getId(),
(int)($commentInfo['user_id'] ?? 0), $commentUserId = $commentInfo2->getUserId(),
$commentInfo['username'] ?? '(Deleted User)', ($commentUserId < 1 ? '(Deleted User)' : $commentInfo2->getUser()->getUsername()),
]); ]);
} else { } 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) { if($redirect) {
@ -169,7 +170,7 @@ switch($commentMode) {
} }
echo json_encode([ echo json_encode([
'id' => $commentInfo['comment_id'], 'id' => $commentInfo2->getId(),
]); ]);
break; break;
@ -179,36 +180,27 @@ switch($commentMode) {
break; break;
} }
$commentInfo = comments_post_get($commentId, false); if(!$commentInfo2->isDeleted()) {
if(!$commentInfo) {
echo render_info_or_json($isXHR, "This comment doesn't exist.", 400);
break;
}
if($commentInfo['comment_deleted'] === null) {
echo render_info_or_json($isXHR, "This comment isn't in a deleted state.", 400); echo render_info_or_json($isXHR, "This comment isn't in a deleted state.", 400);
break; break;
} }
if(!comments_post_delete($commentInfo['comment_id'], false)) { $commentInfo2->setDeleted(false);
echo render_info_or_json($isXHR, 'Failed to restore comment.', 500); $commentInfo2->save();
break;
}
audit_log(MSZ_AUDIT_COMMENT_ENTRY_RESTORE, $currentUserId, [ audit_log(MSZ_AUDIT_COMMENT_ENTRY_RESTORE, $currentUserInfo->getId(), [
$commentInfo['comment_id'], $commentInfo2->getId(),
(int)($commentInfo['user_id'] ?? 0), $commentUserId = $commentInfo2->getUserId(),
$commentInfo['username'] ?? '(Deleted User)', ($commentUserId < 1 ? '(Deleted User)' : $commentInfo2->getUser()->getUsername()),
]); ]);
if($redirect) { if($redirect) {
redirect($redirect . '#comment-' . $commentInfo['comment_id']); redirect($redirect . '#comment-' . $commentInfo2->getId());
break; break;
} }
echo json_encode([ echo json_encode([
'id' => $commentInfo['comment_id'], 'id' => $commentInfo2->getId(),
]); ]);
break; break;
@ -223,26 +215,30 @@ switch($commentMode) {
break; break;
} }
$categoryId = !empty($_POST['comment']['category']) && is_string($_POST['comment']['category']) ? (int)$_POST['comment']['category'] : 0; try {
$category = comments_category_info($categoryId); $categoryInfo = CommentsCategory::byId(
isset($_POST['comment']['category']) && is_string($_POST['comment']['category'])
if(!$category) { ? (int)$_POST['comment']['category']
: 0
);
} catch(CommentsCategoryNotFoundException $ex) {
echo render_info_or_json($isXHR, 'This comment category doesn\'t exist.', 404); echo render_info_or_json($isXHR, 'This comment category doesn\'t exist.', 404);
break; 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); echo render_info_or_json($isXHR, 'This comment category has been locked.', 403);
break; break;
} }
$commentText = !empty($_POST['comment']['text']) && is_string($_POST['comment']['text']) ? $_POST['comment']['text'] : ''; $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'];
$commentReply = !empty($_POST['comment']['reply']) && is_string($_POST['comment']['reply']) ? (int)$_POST['comment']['reply'] : 0; $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) { if($commentLock) {
comments_category_lock($categoryId, is_null($category['category_locked'])); $categoryInfo->setLocked(!$categoryInfo->isLocked());
$categoryInfo->save();
} }
if(strlen($commentText) > 0) { if(strlen($commentText) > 0) {
@ -261,30 +257,53 @@ switch($commentMode) {
break; break;
} }
if($commentReply > 0 && !comments_post_exists($commentReply)) { if($commentReply > 0) {
echo render_info_or_json($isXHR, 'The comment you tried to reply to does not exist.', 404); try {
break; $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( $commentInfo2 = (new CommentsPost)
user_session_current('user_id', 0), ->setUser($currentUserInfo)
$categoryId, ->setCategory($categoryInfo)
$commentText, ->setParsedText($commentText)
$commentPin, ->setPinned($commentPin);
$commentReply
);
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); echo render_info_or_json($isXHR, 'Something went horribly wrong.', 500);
break; break;
} }
if($redirect) { if($redirect) {
redirect($redirect . '#comment-' . $commentId); redirect($redirect . '#comment-' . $commentInfo2->getId());
break; 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; break;
default: default:

View file

@ -72,9 +72,13 @@ $responseStatus = $response->getStatusCode();
header('HTTP/' . $response->getProtocolVersion() . ' ' . $responseStatus . ' ' . $response->getReasonPhrase()); header('HTTP/' . $response->getProtocolVersion() . ' ' . $responseStatus . ' ' . $response->getReasonPhrase());
foreach($response->getHeaders() as $headerName => $headerSet) foreach($response->getHeaders() as $name => $lines) {
foreach($headerSet as $headerLine) $firstLine = true;
header("{$headerName}: {$headerLine}"); foreach($lines as $line) {
header("{$name}: {$line}", $firstLine);
$firstLine = false;
}
}
$responseBody = $response->getBody(); $responseBody = $response->getBody();

View file

@ -3,6 +3,7 @@ namespace Misuzu;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
require_once '../misuzu.php'; 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'] : ''; $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); $isEditing = !empty($_GET['edit']) && is_string($_GET['edit']) ? (bool)$_GET['edit'] : !empty($_POST) && is_array($_POST);
$profileUser = User::findForProfile($userId); try {
$profileUser = User::findForProfile($userId);
if(empty($profileUser)) { } catch(UserNotFoundException $ex) {
http_response_code(404); http_response_code(404);
Template::render('profile.index'); Template::render('profile.index');
return; return;
@ -22,9 +23,9 @@ $notices = [];
$currentUserId = user_session_current('user_id', 0); $currentUserId = user_session_current('user_id', 0);
$viewingAsGuest = $currentUserId === 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]; $userPerms = perms_get_user($currentUserId)[MSZ_PERMS_USER];
$canManageWarnings = perms_check($userPerms, MSZ_PERM_USER_MANAGE_WARNINGS); $canManageWarnings = perms_check($userPerms, MSZ_PERM_USER_MANAGE_WARNINGS);
$canEdit = !$isBanned $canEdit = !$isBanned
@ -34,7 +35,7 @@ $canEdit = !$isBanned
|| user_check_super($currentUserId) || user_check_super($currentUserId)
|| ( || (
perms_check($userPerms, MSZ_PERM_USER_MANAGE_USERS) 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']; $notices[] = MSZ_TMP_USER_ERROR_STRINGS['about']['not-allowed'];
} else { } else {
$setAboutError = user_set_about_page( $setAboutError = user_set_about_page(
$profileUser->user_id, $profileUser->getId(),
$_POST['about']['text'] ?? '', $_POST['about']['text'] ?? '',
(int)($_POST['about']['parser'] ?? Parser::PLAIN) (int)($_POST['about']['parser'] ?? Parser::PLAIN)
); );
@ -106,7 +107,7 @@ if($isEditing) {
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['signature']['not-allowed']; $notices[] = MSZ_TMP_USER_ERROR_STRINGS['signature']['not-allowed'];
} else { } else {
$setSignatureError = user_set_signature( $setSignatureError = user_set_signature(
$profileUser->user_id, $profileUser->getId(),
$_POST['signature']['text'] ?? '', $_POST['signature']['text'] ?? '',
(int)($_POST['signature']['parser'] ?? Parser::PLAIN) (int)($_POST['signature']['parser'] ?? Parser::PLAIN)
); );
@ -125,7 +126,7 @@ if($isEditing) {
$notices[] = "You aren't allow to change your birthdate."; $notices[] = "You aren't allow to change your birthdate.";
} else { } else {
$setBirthdate = user_set_birthdate( $setBirthdate = user_set_birthdate(
$profileUser->user_id, $profileUser->getId(),
(int)($_POST['birthdate']['day'] ?? 0), (int)($_POST['birthdate']['day'] ?? 0),
(int)($_POST['birthdate']['month'] ?? 0), (int)($_POST['birthdate']['month'] ?? 0),
(int)($_POST['birthdate']['year'] ?? 0) (int)($_POST['birthdate']['year'] ?? 0)
@ -154,7 +155,7 @@ if($isEditing) {
if(!empty($_FILES['avatar'])) { if(!empty($_FILES['avatar'])) {
if(!empty($_POST['avatar']['delete'])) { if(!empty($_POST['avatar']['delete'])) {
user_avatar_delete($profileUser->user_id); user_avatar_delete($profileUser->getId());
} else { } else {
if(!$perms['edit_avatar']) { if(!$perms['edit_avatar']) {
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['avatar']['not-allowed']; $notices[] = MSZ_TMP_USER_ERROR_STRINGS['avatar']['not-allowed'];
@ -172,7 +173,7 @@ if($isEditing) {
); );
} else { } else {
$setAvatar = user_avatar_set_from_path( $setAvatar = user_avatar_set_from_path(
$profileUser->user_id, $profileUser->getId(),
$_FILES['avatar']['tmp_name']['file'], $_FILES['avatar']['tmp_name']['file'],
$avatarProps $avatarProps
); );
@ -194,8 +195,8 @@ if($isEditing) {
if(!empty($_FILES['background'])) { if(!empty($_FILES['background'])) {
if((int)($_POST['background']['attach'] ?? -1) === 0) { if((int)($_POST['background']['attach'] ?? -1) === 0) {
user_background_delete($profileUser->user_id); user_background_delete($profileUser->getId());
user_background_set_settings($profileUser->user_id, MSZ_USER_BACKGROUND_ATTACHMENT_NONE); user_background_set_settings($profileUser->getId(), MSZ_USER_BACKGROUND_ATTACHMENT_NONE);
} else { } else {
if(!$perms['edit_background']) { if(!$perms['edit_background']) {
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['background']['not-allowed']; $notices[] = MSZ_TMP_USER_ERROR_STRINGS['background']['not-allowed'];
@ -213,7 +214,7 @@ if($isEditing) {
); );
} else { } else {
$setBackground = user_background_set_from_path( $setBackground = user_background_set_from_path(
$profileUser->user_id, $profileUser->getId(),
$_FILES['background']['tmp_name']['file'], $_FILES['background']['tmp_name']['file'],
$backgroundProps $backgroundProps
); );
@ -243,7 +244,7 @@ if($isEditing) {
$backgroundSettings |= MSZ_USER_BACKGROUND_ATTRIBUTE_SLIDE; $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` ) AS `following_count`
FROM `msz_users` AS u FROM `msz_users` AS u
WHERE `user_id` = :user_id 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() $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)) { if(is_file($backgroundPath)) {
$backgroundInfo = getimagesize($backgroundPath); $backgroundInfo = getimagesize($backgroundPath);
if($backgroundInfo) { if($backgroundInfo) {
Template::set('site_background', [ Template::set('site_background', [
'url' => url('user-background', ['user' => $profileUser->user_id]), 'url' => url('user-background', ['user' => $profileUser->getId()]),
'width' => $backgroundInfo[0], 'width' => $backgroundInfo[0],
'height' => $backgroundInfo[1], 'height' => $backgroundInfo[1],
'settings' => $profileUser->user_background_settings, 'settings' => $profileUser->getBackgroundSettings(),
]); ]);
} }
} }
@ -322,7 +323,7 @@ switch($profileMode) {
case 'following': case 'following':
$template = 'profile.relations'; $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); $followingPagination = new Pagination($followingCount, MSZ_USER_RELATION_FOLLOW_PER_PAGE);
if(!$followingPagination->hasValidOffset()) { if(!$followingPagination->hasValidOffset()) {
@ -331,14 +332,14 @@ switch($profileMode) {
} }
$following = user_relation_users_from( $following = user_relation_users_from(
$profileUser->user_id, MSZ_USER_RELATION_FOLLOW, $profileUser->getId(), MSZ_USER_RELATION_FOLLOW,
$followingPagination->getRange(), $followingPagination->getOffset(), $followingPagination->getRange(), $followingPagination->getOffset(),
$currentUserId $currentUserId
); );
Template::set([ Template::set([
'title' => $profileUser->username . ' / following', 'title' => $profileUser->getUsername() . ' / following',
'canonical_url' => url('user-profile-following', ['user' => $profileUser->user_id]), 'canonical_url' => url('user-profile-following', ['user' => $profileUser->getId()]),
'profile_users' => $following, 'profile_users' => $following,
'profile_relation_pagination' => $followingPagination, 'profile_relation_pagination' => $followingPagination,
]); ]);
@ -346,7 +347,7 @@ switch($profileMode) {
case 'followers': case 'followers':
$template = 'profile.relations'; $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); $followerPagination = new Pagination($followerCount, MSZ_USER_RELATION_FOLLOW_PER_PAGE);
if(!$followerPagination->hasValidOffset()) { if(!$followerPagination->hasValidOffset()) {
@ -355,14 +356,14 @@ switch($profileMode) {
} }
$followers = user_relation_users_to( $followers = user_relation_users_to(
$profileUser->user_id, MSZ_USER_RELATION_FOLLOW, $profileUser->getId(), MSZ_USER_RELATION_FOLLOW,
$followerPagination->getRange(), $followerPagination->getOffset(), $followerPagination->getRange(), $followerPagination->getOffset(),
$currentUserId $currentUserId
); );
Template::set([ Template::set([
'title' => $profileUser->username . ' / followers', 'title' => $profileUser->getUsername() . ' / followers',
'canonical_url' => url('user-profile-followers', ['user' => $profileUser->user_id]), 'canonical_url' => url('user-profile-followers', ['user' => $profileUser->getId()]),
'profile_users' => $followers, 'profile_users' => $followers,
'profile_relation_pagination' => $followerPagination, 'profile_relation_pagination' => $followerPagination,
]); ]);
@ -370,7 +371,7 @@ switch($profileMode) {
case 'forum-topics': case 'forum-topics':
$template = 'profile.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); $topicsPagination = new Pagination($topicsCount, 20);
if(!$topicsPagination->hasValidOffset()) { if(!$topicsPagination->hasValidOffset()) {
@ -379,13 +380,13 @@ switch($profileMode) {
} }
$topics = forum_topic_listing_user( $topics = forum_topic_listing_user(
$profileUser->user_id, $currentUserId, $profileUser->getId(), $currentUserId,
$topicsPagination->getOffset(), $topicsPagination->getRange() $topicsPagination->getOffset(), $topicsPagination->getRange()
); );
Template::set([ Template::set([
'title' => $profileUser->username . ' / topics', 'title' => $profileUser->getUsername() . ' / topics',
'canonical_url' => url('user-profile-forum-topics', ['user' => $profileUser->user_id, 'page' => Pagination::param()]), 'canonical_url' => url('user-profile-forum-topics', ['user' => $profileUser->getId(), 'page' => Pagination::param()]),
'profile_topics' => $topics, 'profile_topics' => $topics,
'profile_topics_pagination' => $topicsPagination, 'profile_topics_pagination' => $topicsPagination,
]); ]);
@ -393,7 +394,7 @@ switch($profileMode) {
case 'forum-posts': case 'forum-posts':
$template = 'profile.posts'; $template = 'profile.posts';
$postsCount = forum_post_count_user($profileUser->user_id); $postsCount = forum_post_count_user($profileUser->getId());
$postsPagination = new Pagination($postsCount, 20); $postsPagination = new Pagination($postsCount, 20);
if(!$postsPagination->hasValidOffset()) { if(!$postsPagination->hasValidOffset()) {
@ -402,7 +403,7 @@ switch($profileMode) {
} }
$posts = forum_post_listing( $posts = forum_post_listing(
$profileUser->user_id, $profileUser->getId(),
$postsPagination->getOffset(), $postsPagination->getOffset(),
$postsPagination->getRange(), $postsPagination->getRange(),
false, false,
@ -410,8 +411,8 @@ switch($profileMode) {
); );
Template::set([ Template::set([
'title' => $profileUser->username . ' / posts', 'title' => $profileUser->getUsername() . ' / posts',
'canonical_url' => url('user-profile-forum-posts', ['user' => $profileUser->user_id, 'page' => Pagination::param()]), 'canonical_url' => url('user-profile-forum-posts', ['user' => $profileUser->getId(), 'page' => Pagination::param()]),
'profile_posts' => $posts, 'profile_posts' => $posts,
'profile_posts_pagination' => $postsPagination, 'profile_posts_pagination' => $postsPagination,
]); ]);
@ -422,7 +423,7 @@ switch($profileMode) {
$warnings = $viewingAsGuest $warnings = $viewingAsGuest
? [] ? []
: user_warning_fetch( : user_warning_fetch(
$profileUser->user_id, $profileUser->getId(),
90, 90,
$canManageWarnings $canManageWarnings
? MSZ_WARN_TYPES_VISIBLE_TO_STAFF ? MSZ_WARN_TYPES_VISIBLE_TO_STAFF

View file

@ -14,7 +14,7 @@ if(!user_session_active()) {
$errors = []; $errors = [];
$currentUserId = user_session_current('user_id'); $currentUserId = user_session_current('user_id');
$currentUser = User::get($currentUserId); $currentUser = User::byId($currentUserId);
$currentEmail = user_email_get($currentUserId); $currentEmail = user_email_get($currentUserId);
$isRestricted = user_warning_check_restriction($currentUserId); $isRestricted = user_warning_check_restriction($currentUserId);
$twoFactorInfo = user_totp_info($currentUserId); $twoFactorInfo = user_totp_info($currentUserId);

View file

@ -27,7 +27,7 @@ function db_to_zip(ZipArchive $archive, int $userId, string $filename, string $q
$errors = []; $errors = [];
$currentUserId = user_session_current('user_id'); $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['action']) && is_string($_POST['action'])) {
if(isset($_POST['password']) && is_string($_POST['password']) if(isset($_POST['password']) && is_string($_POST['password'])

View file

@ -3,15 +3,20 @@ namespace Misuzu;
use Misuzu\Imaging\Image; use Misuzu\Imaging\Image;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
$userAssetsMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : ''; $userAssetsMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$misuzuBypassLockdown = $userAssetsMode === 'avatar'; $misuzuBypassLockdown = $userAssetsMode === 'avatar';
require_once '../misuzu.php'; require_once '../misuzu.php';
$userInfo = User::get((int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT)); try {
$userExists = empty($userExists); $userInfo = User::byId((int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT));
$userId = $userExists ? $userInfo->getUserId() : 0; $userExists = true;
} catch(UserNotFoundException $ex) {
$userExists = false;
}
$userId = $userExists ? $userInfo->getId() : 0;
$canViewImages = !$userExists $canViewImages = !$userExists
|| !user_warning_check_expiration($userId, MSZ_WARN_BAN) || !user_warning_check_expiration($userId, MSZ_WARN_BAN)

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

View file

@ -0,0 +1,6 @@
<?php
namespace Misuzu\Comments;
use Exception;
class CommentsException extends Exception {}

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

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

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

View file

@ -8,7 +8,6 @@ final class DB {
private static $instance; private static $instance;
public const PREFIX = 'msz_'; public const PREFIX = 'msz_';
public const QUERY_SELECT = 'SELECT %2$s FROM `' . self::PREFIX . '%1$s` AS %1$s';
public const ATTRS = [ public const ATTRS = [
PDO::ATTR_CASE => PDO::CASE_NATURAL, PDO::ATTR_CASE => PDO::CASE_NATURAL,
@ -16,11 +15,8 @@ final class DB {
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false, PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => " PDO::MYSQL_ATTR_INIT_COMMAND => 'SET SESSION time_zone = \'+00:00\''
SET SESSION . ', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\'',
sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION',
time_zone = '+00:00';
",
]; ];
public static function init(...$args) { public static function init(...$args) {

View file

@ -1,54 +1,63 @@
<?php <?php
namespace Misuzu\Debug; namespace Misuzu\Debug;
class Stopwatch { final class Stopwatch {
private $startTime = 0; private $startTime = 0;
private $stopTime = 0; private $stopTime = 0;
private $laps = []; private $laps = [];
private static $instance = null; private static $instance = null;
public static function __callStatic(string $name, array $args) { public function __call(string $name, array $args) {
if(self::$instance === null) if($name[0] === '_')
self::$instance = new static; return null;
return self::$instance->{substr($name, 1)}(...$args); 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() { private static function time() {
return microtime(true); return microtime(true);
} }
public function start(): void { public function _start(): void {
$this->startTime = self::time(); $this->startTime = self::time();
} }
public function lap(string $text): void { public function _lap(string $text): void {
$this->laps[$text] = self::time(); $this->laps[$text] = self::time();
} }
public function stop(): void { public function _stop(): void {
$this->stopTime = self::time(); $this->stopTime = self::time();
} }
public function reset(): void { public function _reset(): void {
$this->laps = []; $this->laps = [];
$this->startTime = 0; $this->startTime = 0;
$this->stopTime = 0; $this->stopTime = 0;
} }
public function elapsed(): float { public function _elapsed(): float {
return $this->stopTime - $this->startTime; return $this->stopTime - $this->startTime;
} }
public function laps(): array { public function _laps(): array {
$laps = []; $laps = [];
foreach($this->laps as $name => $time)
foreach($this->laps as $name => $time) {
$laps[$name] = $time - $this->startTime; $laps[$name] = $time - $this->startTime;
}
return $laps; 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);
}
} }

View file

@ -15,6 +15,8 @@ use Misuzu\News\NewsPost;
use Misuzu\News\NewsCategoryNotFoundException; use Misuzu\News\NewsCategoryNotFoundException;
use Misuzu\News\NewsPostNotException; use Misuzu\News\NewsPostNotException;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
final class NewsHandler extends Handler { final class NewsHandler extends Handler {
public function index(HttpResponse $response, HttpRequest $request) { public function index(HttpResponse $response, HttpRequest $request) {
@ -42,11 +44,9 @@ final class NewsHandler extends Handler {
if(!$categoryPagination->hasValidOffset()) if(!$categoryPagination->hasValidOffset())
return 404; return 404;
$posts = NewsPost::byCategory($categoryInfo, $categoryPagination);
$response->setTemplate('news.category', [ $response->setTemplate('news.category', [
'category_info' => $categoryInfo, 'category_info' => $categoryInfo,
'posts' => $posts, 'posts' => $categoryInfo->posts($categoryPagination),
'news_pagination' => $categoryPagination, 'news_pagination' => $categoryPagination,
]); ]);
} }
@ -63,12 +63,16 @@ final class NewsHandler extends Handler {
$postInfo->ensureCommentsSection(); $postInfo->ensureCommentsSection();
$commentsInfo = $postInfo->getCommentSection(); $commentsInfo = $postInfo->getCommentSection();
try {
$commentsUser = User::byId(user_session_current('user_id', 0));
} catch(UserNotFoundException $ex) {
$commentsUser = null;
}
$response->setTemplate('news.post', [ $response->setTemplate('news.post', [
'post_info' => $postInfo, 'post_info' => $postInfo,
'comments_perms' => comments_get_perms(user_session_current('user_id', 0)), 'comments_info' => $commentsInfo,
'comments_category' => $commentsInfo, 'comments_user' => $commentsUser,
'comments' => comments_category_get($commentsInfo['category_id'], user_session_current('user_id', 0)),
]); ]);
} }
@ -76,7 +80,7 @@ final class NewsHandler extends Handler {
private function createFeed(string $feedMode, ?NewsCategory $categoryInfo, array $posts): Feed { private function createFeed(string $feedMode, ?NewsCategory $categoryInfo, array $posts): Feed {
$hasCategory = !empty($categoryInfo); $hasCategory = !empty($categoryInfo);
$pagination = new Pagination(10); $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) $feed = (new Feed)
->setTitle(Config::get('site.name', Config::TYPE_STR, 'Misuzu') . ' » ' . ($hasCategory ? $categoryInfo->getName() : 'Featured News')) ->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'); $response->setContentType('application/atom+xml; charset=utf-8');
return (new AtomFeedSerializer)->serializeFeed( 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'); $response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed( return (new RssFeedSerializer)->serializeFeed(
self::createFeed('rss', $categoryInfo, NewsPost::byCategory($categoryInfo, new Pagination(10))) self::createFeed('rss', $categoryInfo, $categoryInfo->posts(new Pagination(10)))
); );
} }

View file

@ -246,29 +246,29 @@ final class SockChatHandler extends Handler {
if(!isset($userId) || $userId < 1) if(!isset($userId) || $userId < 1)
return ['success' => false, 'reason' => 'unknown']; 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']; return ['success' => false, 'reason' => 'user'];
$perms = self::PERMS_DEFAULT; $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; $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; $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; $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; $perms |= self::PERMS_MANAGE_FORUM;
return [ return [
'success' => true, 'success' => true,
'user_id' => $userInfo->getUserId(), 'user_id' => $userInfo->getId(),
'username' => $userInfo->getUsername(), 'username' => $userInfo->getUsername(),
'colour_raw' => $userInfo->getColourRaw(), 'colour_raw' => $userInfo->getColourRaw(),
'hierarchy' => $userInfo->getHierarchy(), '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, 'perms' => $perms,
]; ];
} }

27
src/Memoizer.php Normal file
View 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.');
}
}

View file

@ -18,7 +18,8 @@ class NewsCategory implements ArrayAccess {
private $postCount = -1; 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`' 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`'; . ', 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 { 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 { public static function countAll(bool $showHidden = false): int {
return (int)DB::prepare(self::countQueryBase() return (int)DB::prepare(self::countQueryBase()
@ -104,7 +109,7 @@ class NewsCategory implements ArrayAccess {
} }
private static function byQueryBase(): string { 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 { public static function byId(int $categoryId): self {
$getCat = DB::prepare(self::byQueryBase() . ' WHERE `category_id` = :cat_id'); $getCat = DB::prepare(self::byQueryBase() . ' WHERE `category_id` = :cat_id');

View file

@ -3,7 +3,10 @@ namespace Misuzu\News;
use Misuzu\DB; use Misuzu\DB;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class NewsPostException extends NewsException {}; class NewsPostException extends NewsException {};
class NewsPostNotFoundException extends NewsPostException {}; class NewsPostNotFoundException extends NewsPostException {};
@ -24,10 +27,11 @@ class NewsPost {
private $category = null; private $category = null;
private $user = null; private $user = null;
private $userLookedUp = false;
private $comments = null; 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`' 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`' . ', %1$s.`post_is_featured`, %1$s.`post_title`, %1$s.`post_text`'
. ', UNIX_TIMESTAMP(%1$s.`post_scheduled`) AS `post_scheduled`' . ', UNIX_TIMESTAMP(%1$s.`post_scheduled`) AS `post_scheduled`'
@ -46,15 +50,17 @@ class NewsPost {
} }
public function setCategoryId(int $categoryId): self { public function setCategoryId(int $categoryId): self {
$this->category_id = max(1, $categoryId); $this->category_id = max(1, $categoryId);
$this->category = null;
return $this; return $this;
} }
public function getCategory(): NewsCategory { public function getCategory(): NewsCategory {
if($this->category === null && ($catId = $this->getCategoryId()) > 0) if($this->category === null)
$this->category = NewsCategory::byId($catId); $this->category = NewsCategory::byId($this->getCategoryId());
return $this->category; return $this->category;
} }
public function setCategory(NewsCategory $category): self { public function setCategory(NewsCategory $category): self {
$this->category_id = $category->getId(); $this->category_id = $category->getId();
$this->category = $category;
return $this; return $this;
} }
@ -63,41 +69,35 @@ class NewsPost {
} }
public function setUserId(int $userId): self { public function setUserId(int $userId): self {
$this->user_id = $userId < 1 ? null : $userId; $this->user_id = $userId < 1 ? null : $userId;
$this->user = null;
return $this; return $this;
} }
public function getUser(): ?User { public function getUser(): ?User {
if($this->user === null && ($userId = $this->getUserId()) > 0) if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) {
$this->user = User::byId($userId); $this->userLookedUp = true;
try {
$this->user = User::byId($userId);
} catch(UserNotFoundException $ex) {}
}
return $this->user; return $this->user;
} }
public function setUser(?User $user): self { public function setUser(?User $user): self {
$this->user_id = $user === null ? null : $user->getId(); $this->user_id = $user === null ? null : $user->getId();
$this->user = $user;
return $this; return $this;
} }
public function getCommentSectionId(): int { public function getCommentSectionId(): int {
return $this->comment_section_id < 1 ? -1 : $this->comment_section_id; return $this->comment_section_id < 1 ? -1 : $this->comment_section_id;
} }
public function hasCommentsSection(): bool { public function hasCommentSection(): bool {
return $this->getCommentSectionId() > 0; return $this->getCommentSectionId() > 0;
} }
public function getCommentSection() { public function getCommentSection(): CommentsCategory {
if($this->comments === null && ($sectionId = $this->getCommentSectionId()) > 0) if($this->comments === null)
$this->comments = comments_category_info($sectionId); $this->comments = CommentsCategory::byId($this->getCommentSectionId());
return $this->comments; 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 { public function isFeatured(): bool {
return $this->post_is_featured !== 0; return $this->post_is_featured !== 0;
@ -153,24 +153,25 @@ class NewsPost {
return $this->getDeletedTime() >= 0; return $this->getDeletedTime() >= 0;
} }
public function setDeleted(bool $isDeleted): self { public function setDeleted(bool $isDeleted): self {
$this->post_deleted = $isDeleted ? time() : null; if($this->isDeleted() !== $isDeleted)
$this->post_deleted = $isDeleted ? time() : null;
return $this; return $this;
} }
public function ensureCommentsSection(): void { public function ensureCommentsSection(): void {
if($this->hasCommentsSection()) if($this->hasCommentSection())
return; 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 = $this->comments->getId();
$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')
DB::prepare('UPDATE `msz_news_posts` SET `comment_section_id` = :comment_section_id WHERE `post_id` = :post_id') ->execute([
->execute([ 'comment_section_id' => $this->getCommentSectionId(),
'comment_section_id' => $this->getCommentSectionId(), 'post_id' => $this->getId(),
'post_id' => $this->getId(), ]);
]);
}
} }
public function save(): void { public function save(): void {
@ -206,7 +207,7 @@ class NewsPost {
} }
private static function countQueryBase(): string { 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 { public static function countAll(bool $onlyFeatured = false, bool $includeScheduled = false, bool $includeDeleted = false): int {
return (int)DB::prepare(self::countQueryBase() return (int)DB::prepare(self::countQueryBase()
@ -226,7 +227,7 @@ class NewsPost {
} }
private static function byQueryBase(): string { 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 { public static function byId(int $postId): self {
$post = DB::prepare(self::byQueryBase() . ' WHERE `post_id` = :post_id') $post = DB::prepare(self::byQueryBase() . ' WHERE `post_id` = :post_id')

View file

@ -3,9 +3,37 @@ namespace Misuzu\Users;
use Misuzu\Colour; use Misuzu\Colour;
use Misuzu\DB; use Misuzu\DB;
use Misuzu\Memoizer;
use Misuzu\Net\IPAddress; 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 { 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 = ' private const USER_SELECT = '
SELECT u.`user_id`, u.`username`, u.`password`, u.`email`, u.`user_super`, u.`user_title`, 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`, u.`user_country`, u.`user_colour`, u.`display_role`, u.`user_totp_key`,
@ -50,48 +78,17 @@ class User {
if($createUser < 1) if($createUser < 1)
return null; 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 { 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 { public function getUsername(): string {
return $this->username ?? ''; return $this->username;
} }
public function hasColour(): bool {
return isset($this->user_colour);
}
public function getColour(): Colour { public function getColour(): Colour {
return new Colour($this->getColourRaw()); return new Colour($this->getColourRaw());
} }
@ -99,8 +96,12 @@ class User {
return $this->user_colour ?? 0x40000000; return $this->user_colour ?? 0x40000000;
} }
public function getEmailAddress(): string {
return $this->email;
}
public function getHierarchy(): int { 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 { public function hasPassword(): bool {
@ -113,12 +114,12 @@ class User {
return password_needs_rehash($this->password, MSZ_USERS_PASSWORD_HASH_ALGO); return password_needs_rehash($this->password, MSZ_USERS_PASSWORD_HASH_ALGO);
} }
public function setPassword(string $password): void { public function setPassword(string $password): void {
if(!$this->hasUserId()) if(($userId = $this->getId()) < 1)
return; return;
DB::prepare('UPDATE `msz_users` SET `password` = :password WHERE `user_id` = :user_id') DB::prepare('UPDATE `msz_users` SET `password` = :password WHERE `user_id` = :user_id')
->bind('password', password_hash($password, MSZ_USERS_PASSWORD_HASH_ALGO)) ->bind('password', password_hash($password, MSZ_USERS_PASSWORD_HASH_ALGO))
->bind('user_id', $this->user_id) ->bind('user_id', $userId)
->execute(); ->execute();
} }
@ -130,6 +131,9 @@ class User {
return !empty($this->user_totp_key); return !empty($this->user_totp_key);
} }
public function getBackgroundSettings(): int { // Use the below methods instead
return $this->user_background_settings;
}
public function getBackgroundAttachment(): int { public function getBackgroundAttachment(): int {
return $this->user_background_settings & 0x0F; return $this->user_background_settings & 0x0F;
} }
@ -141,9 +145,70 @@ class User {
} }
public function profileFields(bool $filterEmpty = true): array { public function profileFields(bool $filterEmpty = true): array {
if(!$this->hasUserId()) if(($userId = $this->getId()) < 1)
return []; 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;
});
} }
} }

View file

@ -0,0 +1,6 @@
<?php
namespace Misuzu\Users;
use Exception;
class UsersException extends Exception {}

View file

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

View file

@ -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 %} {% set reply_mode = reply_to is not null %}
{% from 'macros.twig' import avatar %} {% from 'macros.twig' import avatar %}
@ -6,17 +6,17 @@
<form class="comment comment--input{% if reply_mode %} comment--reply{% endif %}" <form class="comment comment--input{% if reply_mode %} comment--reply{% endif %}"
method="post" action="{{ url('comment-create') }}" method="post" action="{{ url('comment-create') }}"
id="comment-{{ reply_mode ? 'reply-' ~ reply_to.comment_id : 'create-' ~ category.category_id }}"> id="comment-{{ reply_mode ? 'reply-' ~ reply_to.id : 'create-' ~ category.id }}">
{{ input_hidden('comment[category]', category.category_id) }} {{ input_hidden('comment[category]', category.id) }}
{{ input_csrf() }} {{ input_csrf() }}
{% if reply_mode %} {% if reply_mode %}
{{ input_hidden('comment[reply]', reply_to.comment_id) }} {{ input_hidden('comment[reply]', reply_to.id) }}
{% endif %} {% endif %}
<div class="comment__container"> <div class="comment__container">
<div class="avatar comment__avatar"> <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>
<div class="comment__content"> <div class="comment__content">
<textarea <textarea
@ -24,10 +24,10 @@
name="comment[text]" placeholder="Share your extensive insights..."></textarea> name="comment[text]" placeholder="Share your extensive insights..."></textarea>
<div class="comment__actions"> <div class="comment__actions">
{% if not reply_mode %} {% 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') }} {{ input_checkbox('comment[pin]', 'Pin this comment', false, 'comment__action') }}
{% endif %} {% endif %}
{% if perms.can_lock %} {% if user.commentPerms.can_lock|default(false) %}
{{ input_checkbox('comment[lock]', 'Toggle locked status', false, 'comment__action') }} {{ input_checkbox('comment[lock]', 'Toggle locked status', false, 'comment__action') }}
{% endif %} {% endif %}
{% endif %} {% endif %}
@ -40,110 +40,101 @@
</form> </form>
{% endmacro %} {% endmacro %}
{% macro comments_entry(comment, indent, category, user, perms) %} {% macro comments_entry(comment, indent, category, user) %}
{% from 'macros.twig' import avatar %} {% from 'macros.twig' import avatar %}
{% from '_layout/input.twig' import input_checkbox_raw %} {% from '_layout/input.twig' import input_checkbox_raw %}
{% set is_deleted = comment.comment_deleted is not null %} {% set hide_details = comment.userId < 1 or comment.deleted and not user.commentPerms.can_delete_any|default(false) %}
{% set hide_details = is_deleted and not perms.can_delete_any %}
{% if perms.can_delete_any or (not is_deleted or comment.comment_replies|length > 0) %} {% if user.commentPerms.can_delete_any|default(false) or (not comment.deleted or comment.replies(user)|length > 0) %}
{% set is_pinned = comment.comment_pinned is not null %} <div class="comment{% if comment.deleted %} comment--deleted{% endif %}" id="comment-{{ comment.id }}">
<div class="comment{% if is_deleted %} comment--deleted{% endif %}" id="comment-{{ comment.comment_id }}">
<div class="comment__container"> <div class="comment__container">
{% if hide_details %} {% if hide_details %}
<div class="comment__avatar"> <div class="comment__avatar">
{{ avatar(0, indent > 1 ? 40 : 50) }} {{ avatar(0, indent > 1 ? 40 : 50) }}
</div> </div>
{% else %} {% else %}
<a class="comment__avatar" href="{{ url('user-profile', {'user':comment.user_id}) }}"> <a class="comment__avatar" href="{{ url('user-profile', {'user':comment.user.id}) }}">
{{ avatar(comment.user_id, indent > 1 ? 40 : 50, comment.username) }} {{ avatar(comment.user.id, indent > 1 ? 40 : 50, comment.user.username) }}
</a> </a>
{% endif %} {% endif %}
<div class="comment__content"> <div class="comment__content">
<div class="comment__info"> <div class="comment__info">
{% if not hide_details %} {% if not hide_details %}
<a class="comment__user comment__user--link" <a class="comment__user comment__user--link"
href="{{ url('user-profile', {'user':comment.user_id}) }}" href="{{ url('user-profile', {'user':comment.user.id}) }}"
style="{{ comment.user_colour|html_colour }}">{{ comment.username }}</a> style="--user-colour: {{ comment.user.colour}}">{{ comment.user.username }}</a>
{% endif %} {% endif %}
<a class="comment__link" href="#comment-{{ comment.comment_id }}"> <a class="comment__link" href="#comment-{{ comment.id }}">
<time class="comment__date" <time class="comment__date"
title="{{ comment.comment_created|date('r') }}" title="{{ comment.createdTime|date('r') }}"
datetime="{{ comment.comment_created|date('c') }}"> datetime="{{ comment.createdTime|date('c') }}">
{{ comment.comment_created|time_diff }} {{ comment.createdTime|time_diff }}
</time> </time>
</a> </a>
{% if is_pinned %} {% if comment.pinned %}
<span class="comment__pin">{% apply spaceless %} <span class="comment__pin">{% apply spaceless %}
Pinned Pinned
{% if comment.comment_pinned != comment.comment_created %} {% if comment.pinnedTime != comment.createdTime %}
<time title="{{ comment.comment_pinned|date('r') }}" <time title="{{ comment.pinnedTime|date('r') }}"
datetime="{{ comment.comment_pinned|date('c') }}"> datetime="{{ comment.pinnedTime|date('c') }}">
{{ comment.comment_pinned|time_diff }} {{ comment.pinnedTime|time_diff }}
</time> </time>
{% endif %} {% endif %}
{% endapply %}</span> {% endapply %}</span>
{% endif %} {% endif %}
</div> </div>
<div class="comment__text"> <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>
<div class="comment__actions"> <div class="comment__actions">
{% if not is_deleted and user is not null %} {% if not comment.deleted and user is not null %}
{% if perms.can_vote %} {% if user.commentPerms.can_vote|default(false) %}
{% set like_vote_state = comment.comment_user_vote == constant('MSZ_COMMENTS_VOTE_LIKE') {% set like_vote_state = comment.userVote > 0 ? 0 : 1 %}
? constant('MSZ_COMMENTS_VOTE_INDIFFERENT') {% set dislike_vote_state = comment.userVote < 0 ? 0 : -1 %}
: 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') %}
<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 }}" <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.comment_id,'vote':like_vote_state}) }}"> href="{{ url('comment-vote', {'comment':comment.id,'vote':like_vote_state}) }}">
<!--i class="fas fa-thumbs-up"></i-->
Like Like
{% if comment.comment_likes > 0 %} {% if comment.likes > 0 %}
({{ comment.comment_likes|number_format }}) ({{ comment.likes|number_format }})
{% endif %} {% endif %}
</a> </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 }}" <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.comment_id,'vote':dislike_vote_state}) }}"> href="{{ url('comment-vote', {'comment':comment.id,'vote':dislike_vote_state}) }}">
<!--i class="fas fa-thumbs-down"></i-->
Dislike Dislike
{% if comment.comment_dislikes > 0 %} {% if comment.dislikes > 0 %}
({{ comment.comment_dislikes|number_format }}) ({{ comment.dislikes|number_format }})
{% endif %} {% endif %}
</a> </a>
{% endif %} {% endif %}
{% if perms.can_comment %} {% if user.commentPerms.can_comment|default(false) %}
<label class="comment__action comment__action--link" for="comment-reply-toggle-{{ comment.comment_id }}">Reply</label> <label class="comment__action comment__action--link" for="comment-reply-toggle-{{ comment.id }}">Reply</label>
{% endif %} {% endif %}
{% if perms.can_delete_any or (comment.user_id == user.user_id and perms.can_delete) %} {% 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.comment_id }}" href="{{ url('comment-delete', {'comment':comment.comment_id}) }}">Delete</a> <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 %} {% endif %}
{# if user is not null %} {# if user is not null %}
<a class="comment__action comment__action--link comment__action--hide" href="#">Report</a> <a class="comment__action comment__action--link comment__action--hide" href="#">Report</a>
{% endif #} {% endif #}
{% if comment.comment_reply_to is null and perms.can_pin %} {% 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.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> <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 %} {% endif %}
{% elseif perms.can_delete_any %} {% elseif user.commentPerms.can_delete_any|default(false) %}
<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> <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 %} {% endif %}
</div> </div>
</div> </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 %} {% 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 %} {% 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.comment_id}) }} {{ input_checkbox_raw('', false, 'comment__reply-toggle', '', false, {'id':'comment-reply-toggle-' ~ comment.id}) }}
{{ comments_input(category, user, perms, comment) }} {{ comments_input(category, user, comment) }}
{% endif %} {% endif %}
{% if comment.comment_replies is defined and comment.comment_replies|length > 0 %} {% if comment.replies|length > 0 %}
{% for reply in comment.comment_replies %} {% for reply in comment.replies %}
{{ comments_entry(reply, indent + 1, category, user, perms) }} {{ comments_entry(reply, indent + 1, category, user) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>
@ -151,34 +142,34 @@
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro comments_section(comments, category, user, perms) %} {% macro comments_section(category, user) %}
<div class="comments" id="comments"> <div class="comments" id="comments">
<div class="comments__input"> <div class="comments__input">
{% if user|default(null) is null %} {% if user|default(null) is null %}
<div class="comments__notice"> <div class="comments__notice">
Please <a href="{{ url('auth-login') }}" class="comments__notice__link">login</a> to comment. Please <a href="{{ url('auth-login') }}" class="comments__notice__link">login</a> to comment.
</div> </div>
{% elseif category|default(null) is null or perms|default(null) is null %} {% elseif category|default(null) is null %}
<div class="comments__notice"> <div class="comments__notice">
Posting new comments here is disabled. Posting new comments here is disabled.
</div> </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"> <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> </div>
{% elseif not perms.can_comment %} {% elseif not user.commentPerms.can_comment|default(false) %}
<div class="comments__notice"> <div class="comments__notice">
You are not allowed to post comments. You are not allowed to post comments.
</div> </div>
{% else %} {% else %}
{% from _self import comments_input %} {% from _self import comments_input %}
{{ comments_input(category, user, perms) }} {{ comments_input(category, user) }}
{% endif %} {% endif %}
</div> </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"> <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> </div>
{% endif %} {% endif %}
@ -189,13 +180,13 @@
</noscript> </noscript>
<div class="comments__listing"> <div class="comments__listing">
{% if comments|length > 0 %} {% if category.posts|length > 0 %}
{% from _self import comments_entry %} {% from _self import comments_entry %}
{% for comment in comments %} {% for comment in category.posts(user) %}
{{ comments_entry(comment, 1, category, user, perms) }} {{ comments_entry(comment, 1, category, user) }}
{% endfor %} {% endfor %}
{% else %} {% 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. There are no comments yet.
</div> </div>
{% endif %} {% endif %}

View file

@ -83,10 +83,10 @@
</div> </div>
</div> </div>
{% if comments is defined %} {% if comments_category is defined %}
<div class="container"> <div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments for ' ~ change.change_date) }} {{ 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> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -34,10 +34,10 @@
{% endif %} {% endif %}
</div> </div>
{% if comments is defined %} {% if comments_category is defined %}
<div class="container"> <div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments') }} {{ 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> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -38,7 +38,7 @@
<div class="news__preview__links"> <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', {'post': post.id}) }}" class="news__preview__link">Continue reading</a>
<a href="{{ url('news-post-comments', {'post': post.id}) }}" class="news__preview__link"> <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> </a>
</div> </div>
</div> </div>

View file

@ -10,10 +10,10 @@
{% block content %} {% block content %}
{{ news_post(post_info) }} {{ news_post(post_info) }}
{% if comments is defined %} {% if comments_info is defined %}
<div class="container"> <div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments') }} {{ 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> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -4,7 +4,7 @@
{% from '_layout/input.twig' import input_hidden, input_csrf, input_text, input_checkbox, input_file, input_file_raw, input_select %} {% 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 %} {% 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 %} {% set title = profile_user.username %}
{% else %} {% else %}
{% set title = 'User not found!' %} {% set title = 'User not found!' %}
@ -12,7 +12,7 @@
{% block content %} {% block content %}
{% if profile_is_editing %} {% 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') }} {{ input_csrf('profile') }}
{% if perms.edit_avatar %} {% if perms.edit_avatar %}
@ -20,7 +20,7 @@
<script> <script>
function updateAvatarPreview(name, url, preview) { 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 = preview || document.getElementById('avatar-preview');
preview.src = url; preview.src = url;
preview.title = name; preview.title = name;
@ -211,7 +211,7 @@
{% if profile_warnings|length > 0 or profile_warnings_can_manage %} {% if profile_warnings|length > 0 or profile_warnings_can_manage %}
<div class="container profile__container profile__warning__container" id="account-standing"> <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">
<div class="profile__warning__background"></div> <div class="profile__warning__background"></div>

View file

@ -1,8 +1,8 @@
{% extends 'master.twig' %} {% extends 'master.twig' %}
{% if profile_user is defined %} {% if profile_user is defined %}
{% set image = url('user-avatar', {'user': profile_user.user_id, 'res': 200}) %} {% set image = url('user-avatar', {'user': profile_user.id, 'res': 200}) %}
{% set manage_link = url('manage-user', {'user': profile_user.user_id}) %} {% set manage_link = url('manage-user', {'user': profile_user.id}) %}
{% set stats = [ {% set stats = [
{ {
'title': 'Joined', 'title': 'Joined',
@ -17,25 +17,25 @@
{ {
'title': 'Following', 'title': 'Following',
'value': profile_stats.following_count, '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', 'active': profile_mode == 'following',
}, },
{ {
'title': 'Followers', 'title': 'Followers',
'value': profile_stats.followers_count, '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', 'active': profile_mode == 'followers',
}, },
{ {
'title': 'Topics', 'title': 'Topics',
'value': profile_stats.forum_topic_count, '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', 'active': profile_mode == 'forum-topics',
}, },
{ {
'title': 'Posts', 'title': 'Posts',
'value': profile_stats.forum_post_count, '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', 'active': profile_mode == 'forum-posts',
}, },
{ {

View file

@ -1,32 +1,32 @@
<?php <?php
function array_test(array $array, callable $func): bool { function array_test(array $array, callable $func): bool {
foreach($array as $value) { foreach($array as $value)
if(!$func($value)) { if(!$func($value))
return false; return false;
}
}
return true; return true;
} }
function array_apply(array $array, callable $func): array { 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]); $array[$i] = $func($array[$i]);
}
return $array; return $array;
} }
function array_bit_or(array $array1, array $array2): 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; $array1[$key] |= $array2[$key] ?? 0;
}
return $array1; return $array1;
} }
function array_rand_value(array $array) { 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 { 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 { 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'; return '0 B';
}
$divider = $decimal ? 1000 : 1024; $divider = $decimal ? 1000 : 1024;
$exp = floor(log($bytes) / log($divider)); $exp = floor(log($bytes) / log($divider));
$bytes = $bytes / pow($divider, floor($exp)); $bytes = $bytes / pow($divider, $exp);
$symbol = $symbols[$exp]; $symbol = $symbols[$exp];
return sprintf("%.2f %s%sB", $bytes, $symbol, $symbol !== '' && !$decimal ? 'i' : ''); 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 { function safe_delete(string $path): void {
$path = realpath($path); $path = realpath($path);
if(empty($path))
if(empty($path)) {
return; return;
}
if(is_dir($path)) { if(is_dir($path)) {
rmdir($path); rmdir($path);
return; return;
} }
if(is_file($path)) { if(is_file($path))
unlink($path); unlink($path);
}
} }
// mkdir but it fails silently // mkdir but it fails silently
function mkdirs(string $path, bool $recursive = false, int $mode = 0777): bool { function mkdirs(string $path, bool $recursive = false, int $mode = 0777): bool {
if(file_exists($path)) { if(file_exists($path))
return true; return true;
}
return mkdir($path, $mode, $recursive); return mkdir($path, $mode, $recursive);
} }
@ -270,8 +233,8 @@ function html_colour(?int $colour, $attribs = '--user-colour'): string {
return $css; return $css;
} }
function html_avatar(int $userId, int $resolution, string $altText = '', array $attributes = []): string { function html_avatar(?int $userId, int $resolution, string $altText = '', array $attributes = []): string {
$attributes['src'] = url('user-avatar', ['user' => $userId, 'res' => $resolution * 2]); $attributes['src'] = url('user-avatar', ['user' => $userId ?? 0, 'res' => $resolution * 2]);
$attributes['alt'] = $altText; $attributes['alt'] = $altText;
$attributes['class'] = trim('avatar ' . ($attributes['class'] ?? '')); $attributes['class'] = trim('avatar ' . ($attributes['class'] ?? ''));