Rewrote comments backend.

This commit is contained in:
flash 2020-05-18 21:27:34 +00:00
parent 7b212c2c68
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 {
width: 100%;
}
.changelog__action--add { --action-colour: #159635; }
.changelog__action--remove { --action-colour: #e33743; }
.changelog__action--update { --action-colour: #297b8a; }
.changelog__action--fix { --action-colour: #2d5e96; }
.changelog__action--import { --action-colour: #2b9678; }
.changelog__action--revert { --action-colour: #e38245; }
.changelog__entry__datetime {
min-width: 100px;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,7 +27,7 @@ function db_to_zip(ZipArchive $archive, int $userId, string $filename, string $q
$errors = [];
$currentUserId = user_session_current('user_id');
$currentUser = User::get($currentUserId);
$currentUser = User::byId($currentUserId);
if(isset($_POST['action']) && is_string($_POST['action'])) {
if(isset($_POST['password']) && is_string($_POST['password'])

View file

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

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

View file

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

View file

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

View file

@ -246,29 +246,29 @@ final class SockChatHandler extends Handler {
if(!isset($userId) || $userId < 1)
return ['success' => false, 'reason' => 'unknown'];
$userInfo = User::get($userId);
$userInfo = User::byId($userId);
if($userInfo === null || !$userInfo->hasUserId())
if($userInfo === null)
return ['success' => false, 'reason' => 'user'];
$perms = self::PERMS_DEFAULT;
if(perms_check_user(MSZ_PERMS_USER, $userInfo->user_id, MSZ_PERM_USER_MANAGE_USERS))
if(perms_check_user(MSZ_PERMS_USER, $userInfo->getId(), MSZ_PERM_USER_MANAGE_USERS))
$perms |= self::PERMS_MANAGE_USERS;
if(perms_check_user(MSZ_PERMS_USER, $userInfo->user_id, MSZ_PERM_USER_MANAGE_WARNINGS))
if(perms_check_user(MSZ_PERMS_USER, $userInfo->getId(), MSZ_PERM_USER_MANAGE_WARNINGS))
$perms |= self::PERMS_MANAGE_WARNS;
if(perms_check_user(MSZ_PERMS_USER, $userInfo->user_id, MSZ_PERM_USER_CHANGE_BACKGROUND))
if(perms_check_user(MSZ_PERMS_USER, $userInfo->getId(), MSZ_PERM_USER_CHANGE_BACKGROUND))
$perms |= self::PERMS_CHANGE_BACKG;
if(perms_check_user(MSZ_PERMS_FORUM, $userInfo->user_id, MSZ_PERM_FORUM_MANAGE_FORUMS))
if(perms_check_user(MSZ_PERMS_FORUM, $userInfo->getId(), MSZ_PERM_FORUM_MANAGE_FORUMS))
$perms |= self::PERMS_MANAGE_FORUM;
return [
'success' => true,
'user_id' => $userInfo->getUserId(),
'user_id' => $userInfo->getId(),
'username' => $userInfo->getUsername(),
'colour_raw' => $userInfo->getColourRaw(),
'hierarchy' => $userInfo->getHierarchy(),
'is_silenced' => date('c', user_warning_check_expiration($userInfo->getUserId(), MSZ_WARN_SILENCE)),
'is_silenced' => date('c', user_warning_check_expiration($userInfo->getId(), MSZ_WARN_SILENCE)),
'perms' => $perms,
];
}

27
src/Memoizer.php Normal file
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 const TABLE = 'news_categories';
public const TABLE = 'news_categories';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`category_id`, %1$s.`category_name`, %1$s.`category_description`, %1$s.`category_is_hidden`'
. ', UNIX_TIMESTAMP(%1$s.`category_created`) AS `category_created`';
@ -94,8 +95,12 @@ class NewsCategory implements ArrayAccess {
}
}
public function posts(?Pagination $pagination = null, bool $includeScheduled = false, bool $includeDeleted = false): array {
return NewsPost::byCategory($this, $pagination, $includeScheduled, $includeDeleted);
}
private static function countQueryBase(): string {
return sprintf(DB::QUERY_SELECT, self::TABLE, sprintf('COUNT(%s.`category_id`)', self::TABLE));
return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`category_id`)', self::TABLE));
}
public static function countAll(bool $showHidden = false): int {
return (int)DB::prepare(self::countQueryBase()
@ -104,7 +109,7 @@ class NewsCategory implements ArrayAccess {
}
private static function byQueryBase(): string {
return sprintf(DB::QUERY_SELECT, self::TABLE, sprintf(self::SELECT, self::TABLE));
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $categoryId): self {
$getCat = DB::prepare(self::byQueryBase() . ' WHERE `category_id` = :cat_id');

View file

@ -3,7 +3,10 @@ namespace Misuzu\News;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class NewsPostException extends NewsException {};
class NewsPostNotFoundException extends NewsPostException {};
@ -24,10 +27,11 @@ class NewsPost {
private $category = null;
private $user = null;
private $userLookedUp = false;
private $comments = null;
private $commentCount = -1;
private const TABLE = 'news_posts';
public const TABLE = 'news_posts';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`post_id`, %1$s.`category_id`, %1$s.`user_id`, %1$s.`comment_section_id`'
. ', %1$s.`post_is_featured`, %1$s.`post_title`, %1$s.`post_text`'
. ', UNIX_TIMESTAMP(%1$s.`post_scheduled`) AS `post_scheduled`'
@ -46,15 +50,17 @@ class NewsPost {
}
public function setCategoryId(int $categoryId): self {
$this->category_id = max(1, $categoryId);
$this->category = null;
return $this;
}
public function getCategory(): NewsCategory {
if($this->category === null && ($catId = $this->getCategoryId()) > 0)
$this->category = NewsCategory::byId($catId);
if($this->category === null)
$this->category = NewsCategory::byId($this->getCategoryId());
return $this->category;
}
public function setCategory(NewsCategory $category): self {
$this->category_id = $category->getId();
$this->category = $category;
return $this;
}
@ -63,41 +69,35 @@ class NewsPost {
}
public function setUserId(int $userId): self {
$this->user_id = $userId < 1 ? null : $userId;
$this->user = null;
return $this;
}
public function getUser(): ?User {
if($this->user === null && ($userId = $this->getUserId()) > 0)
$this->user = User::byId($userId);
if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) {
$this->userLookedUp = true;
try {
$this->user = User::byId($userId);
} catch(UserNotFoundException $ex) {}
}
return $this->user;
}
public function setUser(?User $user): self {
$this->user_id = $user === null ? null : $user->getId();
$this->user = $user;
return $this;
}
public function getCommentSectionId(): int {
return $this->comment_section_id < 1 ? -1 : $this->comment_section_id;
}
public function hasCommentsSection(): bool {
public function hasCommentSection(): bool {
return $this->getCommentSectionId() > 0;
}
public function getCommentSection() {
if($this->comments === null && ($sectionId = $this->getCommentSectionId()) > 0)
$this->comments = comments_category_info($sectionId);
public function getCommentSection(): CommentsCategory {
if($this->comments === null)
$this->comments = CommentsCategory::byId($this->getCommentSectionId());
return $this->comments;
}
// Temporary solution, should be a method of whatever getCommentSection returns
public function getCommentCount(): int {
if($this->commentCount < 0)
$this->commentCount = (int)DB::prepare('
SELECT COUNT(`comment_id`)
FROM `msz_comments_posts`
WHERE `category_id` = :cat_id
AND `comment_deleted` IS NULL
')->bind('cat_id', $this->getCommentSectionId())->fetchColumn();
return $this->commentCount;
}
public function isFeatured(): bool {
return $this->post_is_featured !== 0;
@ -153,24 +153,25 @@ class NewsPost {
return $this->getDeletedTime() >= 0;
}
public function setDeleted(bool $isDeleted): self {
$this->post_deleted = $isDeleted ? time() : null;
if($this->isDeleted() !== $isDeleted)
$this->post_deleted = $isDeleted ? time() : null;
return $this;
}
public function ensureCommentsSection(): void {
if($this->hasCommentsSection())
if($this->hasCommentSection())
return;
$this->comments = comments_category_create("news-{$this->getId()}");
$this->comments = (new CommentsCategory)
->setName("news-{$this->getId()}");
$this->comments->save();
if($this->comments !== null) {
$this->comment_section_id = (int)$this->comments['category_id'];
DB::prepare('UPDATE `msz_news_posts` SET `comment_section_id` = :comment_section_id WHERE `post_id` = :post_id')
->execute([
'comment_section_id' => $this->getCommentSectionId(),
'post_id' => $this->getId(),
]);
}
$this->comment_section_id = $this->comments->getId();
DB::prepare('UPDATE `msz_news_posts` SET `comment_section_id` = :comment_section_id WHERE `post_id` = :post_id')
->execute([
'comment_section_id' => $this->getCommentSectionId(),
'post_id' => $this->getId(),
]);
}
public function save(): void {
@ -206,7 +207,7 @@ class NewsPost {
}
private static function countQueryBase(): string {
return sprintf(DB::QUERY_SELECT, self::TABLE, sprintf('COUNT(%s.`post_id`)', self::TABLE));
return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`post_id`)', self::TABLE));
}
public static function countAll(bool $onlyFeatured = false, bool $includeScheduled = false, bool $includeDeleted = false): int {
return (int)DB::prepare(self::countQueryBase()
@ -226,7 +227,7 @@ class NewsPost {
}
private static function byQueryBase(): string {
return sprintf(DB::QUERY_SELECT, self::TABLE, sprintf(self::SELECT, self::TABLE));
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $postId): self {
$post = DB::prepare(self::byQueryBase() . ' WHERE `post_id` = :post_id')

View file

@ -3,9 +3,37 @@ namespace Misuzu\Users;
use Misuzu\Colour;
use Misuzu\DB;
use Misuzu\Memoizer;
use Misuzu\Net\IPAddress;
class UserException extends UsersException {} // this naming definitely won't lead to confusion down the line!
class UserNotFoundException extends UserException {}
class User {
// Database fields
// TODO: update all references to use getters and setters and mark all of these as private
public $user_id = -1;
public $username = '';
public $password = '';
public $email = '';
public $register_ip = '::1';
public $last_ip = '::1';
public $user_super = 0;
public $user_country = 'XX';
public $user_colour = null;
public $user_created = null;
public $user_active = null;
public $user_deleted = null;
public $display_role = 1;
public $user_totp_key = null;
public $user_about_content = null;
public $user_about_parser = 0;
public $user_signature_content = null;
public $user_signature_parser = 0;
public $user_birthdate = '';
public $user_background_settings = 0;
public $user_title = null;
private const USER_SELECT = '
SELECT u.`user_id`, u.`username`, u.`password`, u.`email`, u.`user_super`, u.`user_title`,
u.`user_country`, u.`user_colour`, u.`display_role`, u.`user_totp_key`,
@ -50,48 +78,17 @@ class User {
if($createUser < 1)
return null;
return static::get($createUser);
return static::byId($createUser);
}
public static function get(int $userId): ?User { return self::byId($userId); }
public static function byId(int $userId): ?User {
return DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id')
->bind('user_id', $userId)
->fetchObject(User::class);
}
public static function findForLogin(string $usernameOrEmail): ?User {
return DB::prepare(self::USER_SELECT . 'WHERE LOWER(`email`) = LOWER(:email) OR LOWER(`username`) = LOWER(:username)')
->bind('email', $usernameOrEmail)
->bind('username', $usernameOrEmail)
->fetchObject(User::class);
}
public static function findForProfile($userId): ?User {
return DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id OR LOWER(`username`) = LOWER(:username)')
->bind('user_id', (int)$userId)
->bind('username', (string)$userId)
->fetchObject(User::class);
}
public function hasUserId(): bool { return $this->hasId(); }
public function getUserId(): int { return $this->getId(); }
public function hasId(): bool {
return isset($this->user_id) && $this->user_id > 0;
}
public function getId(): int {
return $this->user_id ?? 0;
return $this->user_id < 1 ? -1 : $this->user_id;
}
public function hasUsername(): bool {
return isset($this->username);
}
public function getUsername(): string {
return $this->username ?? '';
return $this->username;
}
public function hasColour(): bool {
return isset($this->user_colour);
}
public function getColour(): Colour {
return new Colour($this->getColourRaw());
}
@ -99,8 +96,12 @@ class User {
return $this->user_colour ?? 0x40000000;
}
public function getEmailAddress(): string {
return $this->email;
}
public function getHierarchy(): int {
return $this->hasUserId() ? user_get_hierarchy($this->getUserId()) : 0;
return ($userId = $this->getId()) < 1 ? 0 : user_get_hierarchy($userId);
}
public function hasPassword(): bool {
@ -113,12 +114,12 @@ class User {
return password_needs_rehash($this->password, MSZ_USERS_PASSWORD_HASH_ALGO);
}
public function setPassword(string $password): void {
if(!$this->hasUserId())
if(($userId = $this->getId()) < 1)
return;
DB::prepare('UPDATE `msz_users` SET `password` = :password WHERE `user_id` = :user_id')
->bind('password', password_hash($password, MSZ_USERS_PASSWORD_HASH_ALGO))
->bind('user_id', $this->user_id)
->bind('user_id', $userId)
->execute();
}
@ -130,6 +131,9 @@ class User {
return !empty($this->user_totp_key);
}
public function getBackgroundSettings(): int { // Use the below methods instead
return $this->user_background_settings;
}
public function getBackgroundAttachment(): int {
return $this->user_background_settings & 0x0F;
}
@ -141,9 +145,70 @@ class User {
}
public function profileFields(bool $filterEmpty = true): array {
if(!$this->hasUserId())
if(($userId = $this->getId()) < 1)
return [];
return ProfileField::user($userId, $filterEmpty);
}
return ProfileField::user($this->user_id, $filterEmpty);
// TODO: Is this the proper location/implementation for this? (no)
private $commentPermsArray = null;
public function commentPerms(): array {
if($this->commentPermsArray === null)
$this->commentPermsArray = perms_check_user_bulk(MSZ_PERMS_COMMENTS, $this->getId(), [
'can_comment' => MSZ_PERM_COMMENTS_CREATE,
'can_delete' => MSZ_PERM_COMMENTS_DELETE_OWN | MSZ_PERM_COMMENTS_DELETE_ANY,
'can_delete_any' => MSZ_PERM_COMMENTS_DELETE_ANY,
'can_pin' => MSZ_PERM_COMMENTS_PIN,
'can_lock' => MSZ_PERM_COMMENTS_LOCK,
'can_vote' => MSZ_PERM_COMMENTS_VOTE,
]);
return $this->commentPermsArray;
}
private static function getMemoizer() {
static $memoizer = null;
if($memoizer === null)
$memoizer = new Memoizer;
return $memoizer;
}
public static function byId(int $userId): ?User {
return self::getMemoizer()->find($userId, function() use ($userId) {
$user = DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id')
->bind('user_id', $userId)
->fetchObject(User::class);
if(!$user)
throw new UserNotFoundException;
return $user;
});
}
public static function findForLogin(string $usernameOrEmail): ?User {
$usernameOrEmailLower = mb_strtolower($usernameOrEmail);
return self::getMemoizer()->find(function() use ($usernameOrEmailLower) {
return mb_strtolower($user->getUsername()) === $usernameOrEmailLower
|| mb_strtolower($user->getEmailAddress()) === $usernameOrEmailLower;
}, function() use ($usernameOrEmail) {
$user = DB::prepare(self::USER_SELECT . 'WHERE LOWER(`email`) = LOWER(:email) OR LOWER(`username`) = LOWER(:username)')
->bind('email', $usernameOrEmail)
->bind('username', $usernameOrEmail)
->fetchObject(User::class);
if(!$user)
throw new UserNotFoundException;
return $user;
});
}
public static function findForProfile($userIdOrName): ?User {
$userIdOrNameLower = mb_strtolower($userIdOrName);
return self::getMemoizer()->find(function() use ($userIdOrNameLower) {
return $user->getId() == $userIdOrNameLower || mb_strtolower($user->getUsername()) === $userIdOrNameLower;
}, function() use ($userIdOrName) {
$user = DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id OR LOWER(`username`) = LOWER(:username)')
->bind('user_id', (int)$userIdOrName)
->bind('username', (string)$userIdOrName)
->fetchObject(User::class);
if(!$user)
throw new UserNotFoundException;
return $user;
});
}
}

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

View file

@ -83,10 +83,10 @@
</div>
</div>
{% if comments is defined %}
{% if comments_category is defined %}
<div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments for ' ~ change.change_date) }}
{{ comments_section(comments, comments_category, current_user|default(null), comments_perms) }}
{{ comments_section(comments_category, comments_user) }}
</div>
{% endif %}
{% endblock %}

View file

@ -34,10 +34,10 @@
{% endif %}
</div>
{% if comments is defined %}
{% if comments_category is defined %}
<div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments') }}
{{ comments_section(comments, comments_category, current_user|default(null), comments_perms) }}
{{ comments_section(comments_category, comments_user) }}
</div>
{% endif %}
{% endblock %}

View file

@ -38,7 +38,7 @@
<div class="news__preview__links">
<a href="{{ url('news-post', {'post': post.id}) }}" class="news__preview__link">Continue reading</a>
<a href="{{ url('news-post-comments', {'post': post.id}) }}" class="news__preview__link">
{{ post.commentCount < 1 ? 'No' : post.commentCount|number_format }} comment{{ post.commentCount != 1 ? 's' : '' }}
{{ not post.hasCommentSection or post.commentSection.postCount < 1 ? 'No' : post.commentSection.postCount|number_format }} comment{{ not post.hasCommentSection or post.commentSection.postCount != 1 ? 's' : '' }}
</a>
</div>
</div>

View file

@ -10,10 +10,10 @@
{% block content %}
{{ news_post(post_info) }}
{% if comments is defined %}
{% if comments_info is defined %}
<div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments') }}
{{ comments_section(comments, comments_category, current_user|default(null), comments_perms) }}
{{ comments_section(comments_info, comments_user) }}
</div>
{% endif %}
{% endblock %}

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 %}
{% if profile_user is defined %}
{% set canonical_url = url('user-profile', {'user': profile_user.user_id}) %}
{% set canonical_url = url('user-profile', {'user': profile_user.id}) %}
{% set title = profile_user.username %}
{% else %}
{% set title = 'User not found!' %}
@ -12,7 +12,7 @@
{% block content %}
{% if profile_is_editing %}
<form class="profile" method="post" action="{{ url('user-profile', {'user': profile_user.user_id}) }}" enctype="multipart/form-data">
<form class="profile" method="post" action="{{ url('user-profile', {'user': profile_user.id}) }}" enctype="multipart/form-data">
{{ input_csrf('profile') }}
{% if perms.edit_avatar %}
@ -20,7 +20,7 @@
<script>
function updateAvatarPreview(name, url, preview) {
url = url || "{{ url('user-avatar', {'user': profile_user.user_id, 'res': 240})|raw }}";
url = url || "{{ url('user-avatar', {'user': profile_user.id, 'res': 240})|raw }}";
preview = preview || document.getElementById('avatar-preview');
preview.src = url;
preview.title = name;
@ -211,7 +211,7 @@
{% if profile_warnings|length > 0 or profile_warnings_can_manage %}
<div class="container profile__container profile__warning__container" id="account-standing">
{{ container_title('Account Standing', false, profile_warnings_can_manage ? url('manage-users-warnings', {'user': profile_user.user_id}) : '') }}
{{ container_title('Account Standing', false, profile_warnings_can_manage ? url('manage-users-warnings', {'user': profile_user.id}) : '') }}
<div class="profile__warning">
<div class="profile__warning__background"></div>

View file

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

View file

@ -1,32 +1,32 @@
<?php
function array_test(array $array, callable $func): bool {
foreach($array as $value) {
if(!$func($value)) {
foreach($array as $value)
if(!$func($value))
return false;
}
}
return true;
}
function array_apply(array $array, callable $func): array {
for($i = 0; $i < count($array); $i++) {
for($i = 0; $i < count($array); ++$i)
$array[$i] = $func($array[$i]);
}
return $array;
}
function array_bit_or(array $array1, array $array2): array {
foreach($array1 as $key => $value) {
foreach($array1 as $key => $value)
$array1[$key] |= $array2[$key] ?? 0;
}
return $array1;
}
function array_rand_value(array $array) {
return $array[array_rand($array)];
return $array[mt_rand(0, count($array) - 1)];
}
function array_find(array $array, callable $callback) {
foreach($array as $item)
if($callback($item))
return $item;
return null;
}
function clamp($num, int $min, int $max): int {
@ -76,72 +76,35 @@ function unique_chars(string $input, bool $multibyte = true): int {
}
function byte_symbol(int $bytes, bool $decimal = false, array $symbols = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']): string {
if($bytes < 1) {
if($bytes < 1)
return '0 B';
}
$divider = $decimal ? 1000 : 1024;
$exp = floor(log($bytes) / log($divider));
$bytes = $bytes / pow($divider, floor($exp));
$bytes = $bytes / pow($divider, $exp);
$symbol = $symbols[$exp];
return sprintf("%.2f %s%sB", $bytes, $symbol, $symbol !== '' && !$decimal ? 'i' : '');
}
// For chat emote list, nuke this when Sharp Chat comms are in this project
function emotes_list(int $hierarchy = PHP_INT_MAX, bool $unique = false, bool $order = true): array {
$getEmotes = \Misuzu\DB::prepare('
SELECT e.`emote_id`, e.`emote_order`, e.`emote_hierarchy`, e.`emote_url`,
s.`emote_string_order`, s.`emote_string`
FROM `msz_emoticons_strings` AS s
LEFT JOIN `msz_emoticons` AS e
ON e.`emote_id` = s.`emote_id`
WHERE `emote_hierarchy` <= :hierarchy
ORDER BY IF(:order, e.`emote_order`, e.`emote_id`), s.`emote_string_order`
');
$getEmotes->bind('hierarchy', $hierarchy);
$getEmotes->bind('order', $order);
$emotes = $getEmotes->fetchAll();
// Removes aliases, emote with lowest ordering is considered the main
if($unique) {
$existing = [];
for($i = 0; $i < count($emotes); $i++) {
if(in_array($emotes[$i]['emote_url'], $existing)) {
unset($emotes[$i]);
} else {
$existing[] = $emotes[$i]['emote_url'];
}
}
}
return $emotes;
}
function safe_delete(string $path): void {
$path = realpath($path);
if(empty($path)) {
if(empty($path))
return;
}
if(is_dir($path)) {
rmdir($path);
return;
}
if(is_file($path)) {
if(is_file($path))
unlink($path);
}
}
// mkdir but it fails silently
function mkdirs(string $path, bool $recursive = false, int $mode = 0777): bool {
if(file_exists($path)) {
if(file_exists($path))
return true;
}
return mkdir($path, $mode, $recursive);
}
@ -270,8 +233,8 @@ function html_colour(?int $colour, $attribs = '--user-colour'): string {
return $css;
}
function html_avatar(int $userId, int $resolution, string $altText = '', array $attributes = []): string {
$attributes['src'] = url('user-avatar', ['user' => $userId, 'res' => $resolution * 2]);
function html_avatar(?int $userId, int $resolution, string $altText = '', array $attributes = []): string {
$attributes['src'] = url('user-avatar', ['user' => $userId ?? 0, 'res' => $resolution * 2]);
$attributes['alt'] = $altText;
$attributes['class'] = trim('avatar ' . ($attributes['class'] ?? ''));