625 lines
21 KiB
PHP
625 lines
21 KiB
PHP
<?php
|
|
namespace Misuzu\Messages;
|
|
|
|
use stdClass;
|
|
use InvalidArgumentException;
|
|
use RuntimeException;
|
|
use Index\XString;
|
|
use Index\Routing\{Route,RouteHandler};
|
|
use Syokuhou\IConfig;
|
|
use Misuzu\{CSRF,Pagination,Perm,Template};
|
|
use Misuzu\Auth\AuthInfo;
|
|
use Misuzu\Parsers\Parser;
|
|
use Misuzu\Perms\Permissions;
|
|
use Misuzu\URLs\{URLInfo,URLRegistry};
|
|
use Misuzu\Users\{UsersContext,UserInfo};
|
|
|
|
class MessagesRoutes extends RouteHandler {
|
|
public const FOLDER_META = [
|
|
'inbox' => [ 'title' => 'Inbox', 'icon' => 'fas fa-inbox fa-fw' ],
|
|
'drafts' => [ 'title' => 'Drafts', 'icon' => 'fas fa-pencil-alt fa-fw' ],
|
|
'sent' => [ 'title' => 'Sent', 'icon' => 'fas fa-paper-plane fa-fw' ],
|
|
'trash' => [ 'title' => 'Trash', 'icon' => 'fas fa-trash-alt fa-fw' ],
|
|
];
|
|
|
|
public function __construct(
|
|
private IConfig $config,
|
|
private URLRegistry $urls,
|
|
private AuthInfo $authInfo,
|
|
private MessagesContext $msgsCtx,
|
|
private UsersContext $usersCtx,
|
|
private Permissions $perms
|
|
) {}
|
|
|
|
private bool $canSendMessages;
|
|
|
|
#[Route('/messages')]
|
|
public function checkAccess($response, $request) {
|
|
// should probably be a permission or something too
|
|
if(!$this->authInfo->isLoggedIn())
|
|
return 401;
|
|
|
|
$globalPerms = $this->authInfo->getPerms('global');
|
|
if(!$globalPerms->check(Perm::G_MESSAGES_VIEW))
|
|
return 403;
|
|
|
|
$this->canSendMessages = $globalPerms->check(Perm::G_MESSAGES_SEND)
|
|
&& !$this->usersCtx->hasActiveBan($this->authInfo->getUserInfo());
|
|
|
|
if($request->getMethod() === 'POST' && $request->isFormContent()) {
|
|
$content = $request->getContent();
|
|
if(!$content->hasParam('_csrfp') || !CSRF::validate((string)$content->getParam('_csrfp')))
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:verify',
|
|
'text' => 'Request verification failed! Refresh the page and try again.',
|
|
],
|
|
];
|
|
|
|
$response->setHeader('X-CSRFP-Token', CSRF::token());
|
|
}
|
|
}
|
|
|
|
private function populateMessage(MessageInfo $messageInfo): object {
|
|
$message = new stdClass;
|
|
|
|
$message->info = $messageInfo;
|
|
$message->author_info = $messageInfo->hasAuthorId() ? $this->usersCtx->getUserInfo($messageInfo->getAuthorId(), 'id') : null;
|
|
$message->author_colour = $this->usersCtx->getUserColour($message->author_info);
|
|
$message->recipient_info = $messageInfo->hasRecipientId() ? $this->usersCtx->getUserInfo($messageInfo->getRecipientId(), 'id') : null;
|
|
$message->recipient_colour = $this->usersCtx->getUserColour($message->recipient_info);
|
|
|
|
return $message;
|
|
}
|
|
|
|
#[Route('GET', '/messages')]
|
|
#[URLInfo('messages-index', '/messages', ['folder' => '<folder>', 'page' => '<page>'])]
|
|
public function getIndex($response, $request, string $folderName = '') {
|
|
$folderName = (string)$request->getParam('folder');
|
|
if($folderName === '')
|
|
$folderName = 'inbox';
|
|
|
|
if(!array_key_exists($folderName, self::FOLDER_META))
|
|
return 404;
|
|
|
|
$folderInbox = $folderName === 'inbox';
|
|
$folderDrafts = $folderName === 'drafts';
|
|
$folderSent = $folderName === 'sent';
|
|
$folderTrash = $folderName === 'trash';
|
|
|
|
$selfInfo = $this->authInfo->getUserInfo();
|
|
$msgsDb = $this->msgsCtx->getDatabase();
|
|
|
|
$authorInfo = !$folderTrash && $folderSent ? $selfInfo : null;
|
|
$recipientInfo = !$folderTrash && $folderInbox ? $selfInfo : null;
|
|
$sent = $folderTrash ? null : !$folderDrafts;
|
|
$deleted = $folderTrash;
|
|
|
|
$pagination = new Pagination($msgsDb->countMessages(
|
|
ownerInfo: $selfInfo,
|
|
authorInfo: $authorInfo,
|
|
recipientInfo: $recipientInfo,
|
|
sent: $sent,
|
|
deleted: $deleted,
|
|
), 50, 'page');
|
|
|
|
$messageInfos = $msgsDb->getMessages(
|
|
ownerInfo: $selfInfo,
|
|
authorInfo: $authorInfo,
|
|
recipientInfo: $recipientInfo,
|
|
sent: $sent,
|
|
deleted: $deleted,
|
|
pagination: $pagination,
|
|
);
|
|
|
|
$messages = [];
|
|
foreach($messageInfos as $messageInfo)
|
|
$messages[] = $this->populateMessage($messageInfo);
|
|
|
|
return Template::renderRaw('messages.index', [
|
|
'can_send_messages' => $this->canSendMessages,
|
|
'folder_name' => $folderName,
|
|
'folder_meta' => self::FOLDER_META,
|
|
'folder_messages' => $messages,
|
|
'folder_pagination' => $pagination,
|
|
]);
|
|
}
|
|
|
|
#[Route('GET', '/messages/stats')]
|
|
#[URLInfo('messages-stats', '/messages/stats')]
|
|
public function getStats() {
|
|
$selfInfo = $this->authInfo->getUserInfo();
|
|
$msgsDb = $this->msgsCtx->getDatabase();
|
|
|
|
return [
|
|
'unread' => $msgsDb->countMessages(
|
|
ownerInfo: $selfInfo,
|
|
recipientInfo: $selfInfo,
|
|
sent: true,
|
|
deleted: false,
|
|
read: false,
|
|
),
|
|
];
|
|
}
|
|
|
|
#[Route('POST', '/messages/recipient')]
|
|
#[URLInfo('messages-recipient', '/messages/recipient')]
|
|
public function postRecipient($response, $request) {
|
|
if(!$request->isFormContent())
|
|
return 400;
|
|
if(!$this->canSendMessages)
|
|
return 403;
|
|
|
|
$content = $request->getContent();
|
|
$name = trim((string)$content->getParam('name'));
|
|
|
|
// flappy hacks
|
|
if(str_starts_with(mb_strtolower($name), 'flappyzor'))
|
|
$name = 14;
|
|
|
|
$userInfo = null;
|
|
if(!empty($name))
|
|
try {
|
|
$userInfo = $this->usersCtx->getUserInfo($name, 'messaging');
|
|
} catch(InvalidArgumentException $ex) {
|
|
} catch(RuntimeException $ex) {}
|
|
|
|
if($userInfo === null)
|
|
return [
|
|
'avatar' => $this->urls->format('user-avatar', [
|
|
'res' => 200,
|
|
]),
|
|
];
|
|
|
|
return [
|
|
'id' => $userInfo->getId(),
|
|
'name' => $userInfo->getName(),
|
|
'avatar' => $this->urls->format('user-avatar', [
|
|
'user' => $userInfo->getId(),
|
|
'res' => 200,
|
|
]),
|
|
];
|
|
}
|
|
|
|
#[Route('GET', '/messages/compose')]
|
|
#[URLInfo('messages-compose', '/messages/compose', ['recipient' => '<recipient>'])]
|
|
public function getEditor($response, $request) {
|
|
if(!$this->canSendMessages)
|
|
return 403;
|
|
|
|
return Template::renderRaw('messages.compose', [
|
|
'recipient' => (string)$request->getParam('recipient'),
|
|
]);
|
|
}
|
|
|
|
#[Route('GET', '/messages/:message')]
|
|
#[URLInfo('messages-view', '/messages/<message>')]
|
|
public function getView($response, $request, string $messageId) {
|
|
if(strlen($messageId) !== 8)
|
|
return 404;
|
|
|
|
$selfInfo = $this->authInfo->getUserInfo();
|
|
$msgsDb = $this->msgsCtx->getDatabase();
|
|
|
|
try {
|
|
$messageInfo = $msgsDb->getMessageInfo($selfInfo, $messageId);
|
|
} catch(RuntimeException $ex) {
|
|
return 404;
|
|
}
|
|
|
|
if(!$messageInfo->isRead())
|
|
$msgsDb->updateMessage(
|
|
ownerInfo: $selfInfo,
|
|
messageInfo: $messageInfo,
|
|
readAt: time(),
|
|
);
|
|
|
|
$message = $this->populateMessage($messageInfo);
|
|
|
|
$replyTo = null;
|
|
if($messageInfo->hasReplyToId()) {
|
|
try {
|
|
$replyTo = $this->populateMessage(
|
|
$msgsDb->getMessageInfo($selfInfo, $messageInfo, true)
|
|
);
|
|
} catch(RuntimeException $ex) {}
|
|
}
|
|
|
|
$repliesForInfos = $msgsDb->getMessages(
|
|
ownerInfo: $selfInfo,
|
|
repliesFor: $messageInfo,
|
|
deleted: false,
|
|
);
|
|
|
|
$draftInfo = null;
|
|
$repliesFor = [];
|
|
foreach($repliesForInfos as $repliesForInfo) {
|
|
$repliesFor[] = $this->populateMessage($repliesForInfo);
|
|
|
|
if(!$repliesForInfo->isSent() && $draftInfo === null)
|
|
$draftInfo = $repliesForInfo;
|
|
}
|
|
|
|
return Template::renderRaw('messages.thread', [
|
|
'can_send_messages' => $this->canSendMessages,
|
|
'self_info' => $selfInfo,
|
|
'reply_to' => $replyTo,
|
|
'message' => $message,
|
|
'draft_info' => $draftInfo,
|
|
'replies_for' => $repliesFor,
|
|
]);
|
|
}
|
|
|
|
private function checkCanReceiveMessages(UserInfo|string $userInfo): ?array {
|
|
$globalPerms = $this->perms->getPermissions('global', $userInfo);
|
|
if(!$globalPerms->check(Perm::G_MESSAGES_VIEW))
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:recipient_cannot_recv',
|
|
'text' => 'This person is not allowed to receive messages.',
|
|
],
|
|
];
|
|
|
|
return null;
|
|
}
|
|
|
|
private function checkMessageFields(string $title, string $body, int $parser): ?array {
|
|
if(!Parser::isValid($parser))
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:invalid_parser',
|
|
'text' => 'Invalid parser selected.',
|
|
],
|
|
];
|
|
|
|
$lengths = $this->config->getValues([
|
|
['title.minLength:i', 1],
|
|
['title.maxLength:i', 200],
|
|
['body.minLength:i', 1],
|
|
['body.maxLength:i', 60000],
|
|
]);
|
|
|
|
$titleLength = mb_strlen(trim($title));
|
|
|
|
if($titleLength < $lengths['title.minLength'])
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:title_too_short',
|
|
'args' => [$lengths['title.minLength'], $titleLength],
|
|
'text' => sprintf('Title may not be shorter than %d characters. You entered %d characters.', $lengths['title.minLength'], $titleLength),
|
|
],
|
|
];
|
|
|
|
if($titleLength > $lengths['title.maxLength'])
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:title_too_long',
|
|
'args' => [$lengths['title.maxLength'], $titleLength],
|
|
'text' => sprintf('Title may not be longer than %d characters. You entered %d characters.', $lengths['title.maxLength'], $titleLength),
|
|
],
|
|
];
|
|
|
|
$bodyLength = mb_strlen(trim($body));
|
|
|
|
if($bodyLength < $lengths['body.minLength'])
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:body_too_short',
|
|
'args' => [$lengths['body.minLength'], $bodyLength],
|
|
'text' => sprintf('Message may not be shorter than %d characters. You entered %d characters.', $lengths['body.minLength'], $bodyLength),
|
|
],
|
|
];
|
|
|
|
if($bodyLength > $lengths['body.maxLength'])
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:body_too_long',
|
|
'args' => [$lengths['body.maxLength'], $bodyLength],
|
|
'text' => sprintf('Message may not be longer than %d characters. You entered %d characters.', $lengths['body.maxLength'], $bodyLength),
|
|
],
|
|
];
|
|
|
|
return null;
|
|
}
|
|
|
|
#[Route('POST', '/messages/create')]
|
|
#[URLInfo('messages-create', '/messages/create')]
|
|
public function postCreate($response, $request) {
|
|
if(!$request->isFormContent())
|
|
return 400;
|
|
if(!$this->canSendMessages)
|
|
return 403;
|
|
|
|
$content = $request->getContent();
|
|
$recipient = (string)$content->getParam('recipient');
|
|
$replyTo = (string)$content->getParam('reply');
|
|
$title = (string)$content->getParam('title');
|
|
$body = (string)$content->getParam('body');
|
|
$parser = (int)$content->getParam('parser', FILTER_SANITIZE_NUMBER_INT);
|
|
$draft = !empty($content->getParam('draft'));
|
|
|
|
$error = $this->checkMessageFields($title, $body, $parser);
|
|
if($error !== null)
|
|
return $error;
|
|
|
|
$selfInfo = $this->authInfo->getUserInfo();
|
|
$msgsDb = $this->msgsCtx->getDatabase();
|
|
|
|
try {
|
|
$recipientInfo = $this->usersCtx->getUserInfo($recipient, 'messaging');
|
|
} catch(InvalidArgumentException $ex) {
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:recipient_invalid',
|
|
'text' => 'Name of the recipient was incorrectly formatted.',
|
|
'jeff' => $recipient,
|
|
],
|
|
];
|
|
} catch(RuntimeException $ex) {
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:recipient_not_found',
|
|
'text' => 'Recipient does not exist.',
|
|
],
|
|
];
|
|
}
|
|
|
|
$error = $this->checkCanReceiveMessages($recipientInfo);
|
|
if($error !== null)
|
|
return $error;
|
|
|
|
$replyToInfo = null;
|
|
if(!empty($replyTo)) {
|
|
try {
|
|
$replyToInfo = $msgsDb->getMessageInfo($selfInfo, $replyTo);
|
|
} catch(RuntimeException $ex) {
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:reply_not_found',
|
|
'text' => 'The message you are trying to reply to does not exist.',
|
|
],
|
|
];
|
|
}
|
|
|
|
if(!$replyToInfo->isSent())
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:draft_reply',
|
|
'text' => 'You cannot reply to a draft.',
|
|
],
|
|
];
|
|
}
|
|
|
|
$msgId = XString::random(8);
|
|
$sentAt = $draft ? null : time();
|
|
|
|
// own copy
|
|
$msgsDb->createMessage(
|
|
messageId: $msgId,
|
|
ownerInfo: $selfInfo,
|
|
authorInfo: $selfInfo,
|
|
recipientInfo: $recipientInfo,
|
|
title: $title,
|
|
body: $body,
|
|
parser: $parser,
|
|
replyTo: $replyToInfo,
|
|
sentAt: $sentAt
|
|
);
|
|
|
|
// recipient copy
|
|
if($sentAt !== null && $recipientInfo->getId() !== $selfInfo->getId())
|
|
$msgsDb->createMessage(
|
|
messageId: $msgId,
|
|
ownerInfo: $recipientInfo,
|
|
authorInfo: $selfInfo,
|
|
recipientInfo: $recipientInfo,
|
|
title: $title,
|
|
body: $body,
|
|
parser: $parser,
|
|
replyTo: $replyToInfo,
|
|
sentAt: $sentAt
|
|
);
|
|
|
|
return [
|
|
'id' => $msgId,
|
|
'url' => $this->urls->format('messages-view', ['message' => $msgId]),
|
|
];
|
|
}
|
|
|
|
#[Route('POST', '/messages/:message')]
|
|
#[URLInfo('messages-update', '/messages/<message>')]
|
|
public function postUpdate($response, $request, string $messageId) {
|
|
if(!$request->isFormContent())
|
|
return 400;
|
|
if(!$this->canSendMessages)
|
|
return 403;
|
|
|
|
$content = $request->getContent();
|
|
$title = (string)$content->getParam('title');
|
|
$body = (string)$content->getParam('body');
|
|
$parser = (int)$content->getParam('parser', FILTER_SANITIZE_NUMBER_INT);
|
|
$draft = !empty($content->getParam('draft'));
|
|
|
|
$error = $this->checkMessageFields($title, $body, $parser);
|
|
if($error !== null)
|
|
return $error;
|
|
|
|
$selfInfo = $this->authInfo->getUserInfo();
|
|
$msgsDb = $this->msgsCtx->getDatabase();
|
|
|
|
try {
|
|
$messageInfo = $msgsDb->getMessageInfo($selfInfo, $messageId);
|
|
} catch(RuntimeException $ex) {
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:edit_not_found',
|
|
'text' => 'The message you are trying to edit does not exist.',
|
|
],
|
|
];
|
|
}
|
|
|
|
if(!$messageInfo->hasAuthorId() || $messageInfo->getAuthorId() !== $selfInfo->getId())
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:not_author',
|
|
'text' => 'You are not the author of this message.',
|
|
],
|
|
];
|
|
|
|
if(!$messageInfo->hasRecipientId())
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:recipient_gone',
|
|
'text' => 'The recipient of this message no longer exists, it cannot be sent or edited.',
|
|
],
|
|
];
|
|
|
|
if($messageInfo->isSent())
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:not_draft',
|
|
'text' => 'You cannot edit a message that has already been sent.',
|
|
],
|
|
];
|
|
|
|
$error = $this->checkCanReceiveMessages($messageInfo->getRecipientId());
|
|
if($error !== null)
|
|
return $error;
|
|
|
|
$sentAt = $draft ? null : time();
|
|
|
|
$msgsDb->updateMessage(
|
|
ownerInfo: $selfInfo,
|
|
messageInfo: $messageInfo,
|
|
title: $title,
|
|
body: $body,
|
|
parser: $parser,
|
|
sentAt: $sentAt,
|
|
);
|
|
|
|
// recipient copy
|
|
if($sentAt !== null && $messageInfo->getRecipientId() !== $selfInfo->getId())
|
|
$msgsDb->createMessage(
|
|
messageId: $messageId,
|
|
ownerInfo: $messageInfo->getRecipientId(),
|
|
authorInfo: $selfInfo,
|
|
recipientInfo: $messageInfo->getRecipientId(),
|
|
title: $title,
|
|
body: $body,
|
|
parser: $parser,
|
|
replyTo: $messageInfo->getReplyToId(),
|
|
sentAt: $sentAt
|
|
);
|
|
|
|
return [
|
|
'id' => $messageId,
|
|
'url' => $this->urls->format('messages-view', ['message' => $messageId]),
|
|
];
|
|
}
|
|
|
|
#[Route('POST', '/messages/mark')]
|
|
#[URLInfo('messages-mark', '/messages/mark')]
|
|
public function postMark($response, $request) {
|
|
if(!$request->isFormContent())
|
|
return 400;
|
|
|
|
$content = $request->getContent();
|
|
$type = (string)$content->getParam('type');
|
|
$messages = explode(',', (string)$content->getParam('messages'));
|
|
|
|
if($type !== 'read' && $type !== 'unread')
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:unsupported_mark',
|
|
'text' => 'Attempting to mark message with an unsupported state.',
|
|
],
|
|
];
|
|
|
|
$selfInfo = $this->authInfo->getUserInfo();
|
|
$msgsDb = $this->msgsCtx->getDatabase();
|
|
|
|
foreach($messages as $messageId)
|
|
$msgsDb->updateMessage(
|
|
ownerInfo: $selfInfo,
|
|
messageInfo: $messageId,
|
|
readAt: $type === 'read' ? time() : null,
|
|
);
|
|
|
|
return [];
|
|
}
|
|
|
|
#[Route('POST', '/messages/delete')]
|
|
#[URLInfo('messages-delete', '/messages/delete')]
|
|
public function postDelete($response, $request) {
|
|
if(!$request->isFormContent())
|
|
return 400;
|
|
|
|
$content = $request->getContent();
|
|
$messages = explode(',', (string)$content->getParam('messages'));
|
|
|
|
if(empty($messages))
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:empty',
|
|
'text' => 'No messages were supplied.',
|
|
],
|
|
];
|
|
|
|
$this->msgsCtx->getDatabase()->deleteMessages(
|
|
$this->authInfo->getUserInfo(),
|
|
$messages
|
|
);
|
|
|
|
return [];
|
|
}
|
|
|
|
#[Route('POST', '/messages/restore')]
|
|
#[URLInfo('messages-restore', '/messages/restore')]
|
|
public function postRestore($response, $request) {
|
|
if(!$request->isFormContent())
|
|
return 400;
|
|
|
|
$content = $request->getContent();
|
|
$messages = explode(',', (string)$content->getParam('messages'));
|
|
|
|
if(empty($messages))
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:empty',
|
|
'text' => 'No messages were supplied.',
|
|
],
|
|
];
|
|
|
|
$this->msgsCtx->getDatabase()->restoreMessages(
|
|
$this->authInfo->getUserInfo(),
|
|
$messages
|
|
);
|
|
|
|
return [];
|
|
}
|
|
|
|
#[Route('POST', '/messages/nuke')]
|
|
#[URLInfo('messages-nuke', '/messages/nuke')]
|
|
public function postNuke($response, $request) {
|
|
if(!$request->isFormContent())
|
|
return 400;
|
|
|
|
$content = $request->getContent();
|
|
$messages = explode(',', (string)$content->getParam('messages'));
|
|
|
|
if(empty($messages))
|
|
return [
|
|
'error' => [
|
|
'name' => 'msgs:empty',
|
|
'text' => 'No messages were supplied.',
|
|
],
|
|
];
|
|
|
|
$this->msgsCtx->getDatabase()->nukeMessages(
|
|
$this->authInfo->getUserInfo(),
|
|
$messages
|
|
);
|
|
|
|
return [];
|
|
}
|
|
}
|