'IS NULL'],
['fp.read_forum' => '1']
];
if ($tid) {
$curPosting['select'] = ['f.id', 'f.forum_name', 'f.moderators', 'f.redirect_url', 'fp.post_replies', 'fp.post_topics', 't.subject', 't.closed', 'is_subscribed' => 's.user_id'];
$curPosting = DB::table('topics')
->tableAlias('t')
->selectMany($curPosting['select'])
->innerJoin('forums', ['f.id', '=', 't.forum_id'], 'f')
->leftOuterJoin('forum_perms', 'fp.forum_id=f.id AND fp.group_id='.User::get()->g_id, 'fp')
->leftOuterJoin('topic_subscriptions', 't.id=s.topic_id AND s.user_id='.User::get()->id, 's')
->whereAnyIs($curPosting['where'])
->where('t.id', $tid);
} else {
$curPosting['select'] = ['f.id', 'f.forum_name', 'f.moderators', 'f.redirect_url', 'fp.post_replies', 'fp.post_topics'];
$curPosting = DB::table('forums')
->tableAlias('f')
->selectMany($curPosting['select'])
->leftOuterJoin('forum_perms', 'fp.forum_id=f.id AND fp.group_id='.User::get()->g_id, 'fp')
->whereAnyIs($curPosting['where'])
->where('f.id', $fid);
}
$curPosting = Hooks::fireDB('model.post.get_info_post_query', $curPosting);
$curPosting = $curPosting->findOne();
if (!$curPosting) {
throw new Error(__('Bad request'), 404);
}
$curPosting = Hooks::fire('model.post.get_info_post', $curPosting);
return $curPosting;
}
// Fetch some info about the post, the topic and the forum
public function getInfoEdit($id)
{
$id = Hooks::fire('model.post.get_info_edit_start', $id);
$curPost['select'] = ['fid' => 'f.id', 'f.forum_name', 'f.moderators', 'f.redirect_url', 'fp.post_topics', 'tid' => 't.id', 't.subject', 't.posted', 't.first_post_id', 't.sticky', 't.closed', 'p.poster', 'p.poster_id', 'p.message', 'p.hide_smilies'];
$curPost['where'] = [
['fp.read_forum' => 'IS NULL'],
['fp.read_forum' => '1']
];
$curPost = DB::table('posts')
->tableAlias('p')
->selectMany($curPost['select'])
->innerJoin('topics', ['t.id', '=', 'p.topic_id'], 't')
->innerJoin('forums', ['f.id', '=', 't.forum_id'], 'f')
->leftOuterJoin('forum_perms', 'fp.forum_id=f.id AND fp.group_id='.User::get()->g_id, 'fp')
->whereAnyIs($curPost['where'])
->where('p.id', $id);
$curPost = Hooks::fireDB('model.post.get_info_edit_query', $curPost);
$curPost = $curPost->findOne();
if (!$curPost) {
throw new Error(__('Bad request'), 400);
}
return $curPost;
}
// Checks the post for errors before posting
public function checkErrorsPost($fid, $errors)
{
$langAntispamQuestions = require ForumEnv::get('FEATHER_ROOT').'featherbb/lang/'.User::getPref('language').'/antispam.php';
$fid = Hooks::fire('model.post.check_errors_before_post_start', $fid);
// Antispam feature
if (User::get()->is_guest) {
// It's a guest, so we have to validate the username
$profile = new \FeatherBB\Model\Profile();
$errors = $profile->checkUsername(Utils::trim(Input::post('req_username')), $errors);
$errors = Hooks::fire('model.post.check_errors_before_post_antispam', $errors);
$question = Input::post('captcha_q') ? trim(Input::post('captcha_q')) : '';
$answer = Input::post('captcha') ? strtoupper(trim(Input::post('captcha'))) : '';
$langAntispamQuestionsArray = [];
foreach ($langAntispamQuestions as $k => $v) {
$langAntispamQuestionsArray[md5($k)] = strtoupper($v);
}
if (empty($langAntispamQuestionsArray[$question]) || $langAntispamQuestionsArray[$question] != $answer) {
$errors[] = __('Robot test fail');
}
}
// Flood protection
if (Input::post('preview') != '' && User::get()->last_post != '' && (time() - User::get()->last_post) < User::getPref('post.min_interval')) {
$errors[] = sprintf(__('Flood start'), User::getPref('post.min_interval'), User::getPref('post.min_interval') - (time() - User::get()->last_post));
}
// If it's a new topic
if ($fid) {
$subject = Utils::trim(Input::post('req_subject'));
$subject = Hooks::fire('model.post.check_errors_before_new_topic_subject', $subject);
if (ForumSettings::get('o_censoring') == '1') {
$censoredSubject = Utils::trim(Utils::censor($subject));
$censoredSubject = Hooks::fire('model.post.check_errors_before_censored', $censoredSubject);
}
if ($subject == '') {
$errors[] = __('No subject');
} elseif (ForumSettings::get('o_censoring') == '1' && $censoredSubject == '') {
$errors[] = __('No subject after censoring');
} elseif (Utils::strlen($subject) > 70) {
$errors[] = __('Too long subject');
} elseif (ForumSettings::get('p_subject_all_caps') == '0' && Utils::isAllUppercase($subject) && !User::isAdminMod()) {
$errors[] = __('All caps subject');
}
$errors = Hooks::fire('model.post.check_errors_before_new_topic_errors', $errors);
}
if (User::get()->is_guest) {
$email = strtolower(Utils::trim((ForumSettings::get('p_force_guest_email') == '1') ? Input::post('req_email') : Input::post('email')));
if (ForumSettings::get('p_force_guest_email') == '1' || $email != '') {
$errors = Hooks::fire('model.post.check_errors_before_post_email', $errors, $email);
if (!Email::isValidEmail($email)) {
$errors[] = __('Invalid email');
}
// Check if it's a banned email address
// we should only check guests because members' addresses are already verified
if (User::get()->is_guest && Email::isBannedEmail($email)) {
if (ForumSettings::get('p_allow_banned_email') == '0') {
$errors[] = __('Banned email');
}
$errors['banned_email'] = 1; // Used later when we send an alert email
}
}
}
// Clean up message from POST
$message = Utils::linebreaks(Utils::trim(Input::post('req_message')));
$message = Hooks::fire('model.post.check_errors_before_post_message', $message);
// Here we use strlen() not Utils::strlen() as we want to limit the post to FEATHER_MAX_POSTSIZE bytes, not characters
if (strlen($message) > ForumEnv::get('FEATHER_MAX_POSTSIZE')) {
$errors[] = sprintf(__('Too long message'), Utils::forumNumberFormat(ForumEnv::get('FEATHER_MAX_POSTSIZE')));
} elseif (ForumSettings::get('p_message_all_caps') == '0' && Utils::isAllUppercase($message) && !User::isAdminMod()) {
$errors[] = __('All caps message');
}
// Validate BBCode syntax
if (ForumSettings::get('p_message_bbcode') == '1') {
$message = Parser::preparseBbcode($message, $errors);
$message = Hooks::fire('model.post.check_errors_before_post_bbcode', $message);
}
if (empty($errors)) {
$errors = Hooks::fire('model.post.check_errors_before_post_no_error', $errors);
if ($message == '') {
$errors[] = __('No message');
} elseif (ForumSettings::get('o_censoring') == '1') {
// Censor message to see if that causes problems
$censoredMessage = Utils::trim(Utils::censor($message));
if ($censoredMessage == '') {
$errors[] = __('No message after censoring');
}
}
}
$errors = Hooks::fire('model.post.check_errors_before_post', $errors);
return $errors;
}
public static function checkErrorsEdit($canEditSubject, $errors, $isAdmmod)
{
$errors = Hooks::fire('model.post.check_errors_before_edit_start', $errors);
// If it's a topic it must contain a subject
if ($canEditSubject) {
$subject = Utils::trim(Input::post('req_subject'));
if (ForumSettings::get('o_censoring') == '1') {
$censoredSubject = Utils::trim(Utils::censor($subject));
}
if ($subject == '') {
$errors[] = __('No subject');
} elseif (ForumSettings::get('o_censoring') == '1' && $censoredSubject == '') {
$errors[] = __('No subject after censoring');
} elseif (Utils::strlen($subject) > 70) {
$errors[] = __('Too long subject');
} elseif (ForumSettings::get('p_subject_all_caps') == '0' && Utils::isAllUppercase($subject) && !$isAdmmod) {
$errors[] = __('All caps subject');
}
}
// Clean up message from POST
$message = Utils::linebreaks(Utils::trim(Input::post('req_message')));
// Here we use strlen() not Utils::strlen() as we want to limit the post to FEATHER_MAX_POSTSIZE bytes, not characters
if (strlen($message) > ForumEnv::get('FEATHER_MAX_POSTSIZE')) {
$errors[] = sprintf(__('Too long message'), Utils::forumNumberFormat(ForumEnv::get('FEATHER_MAX_POSTSIZE')));
} elseif (ForumSettings::get('p_message_all_caps') == '0' && Utils::isAllUppercase($message) && !$isAdmmod) {
$errors[] = __('All caps message');
}
// Validate BBCode syntax
if (ForumSettings::get('p_message_bbcode') == '1') {
$message = Parser::preparseBbcode($message, $errors);
}
if (empty($errors)) {
if ($message == '') {
$errors[] = __('No message');
} elseif (ForumSettings::get('o_censoring') == '1') {
// Censor message to see if that causes problems
$censoredMessage = Utils::trim(Utils::censor($message));
if ($censoredMessage == '') {
$errors[] = __('No message after censoring');
}
}
}
$errors = Hooks::fire('model.post.check_errors_before_edit', $errors);
return $errors;
}
// If the previous check went OK, setup some variables used later
public function setupVariables($errors, $isAdmmod)
{
$post = [];
$post = Hooks::fire('model.post.setup_variables_start', $post, $errors, $isAdmmod);
if (!User::get()->is_guest) {
$post['username'] = User::get()->username;
$post['email'] = User::get()->email;
}
// Otherwise it should be in $feather ($_pOST)
else {
$post['username'] = Utils::trim(Input::post('req_username'));
$post['email'] = strtolower(Utils::trim((ForumSettings::get('p_force_guest_email') == '1') ? Input::post('req_email') : Input::post('email')));
}
if (Input::post('req_subject')) {
$post['subject'] = Utils::trim(Input::post('req_subject'));
}
$post['hide_smilies'] = Input::post('hide_smilies') ? '1' : '0';
$post['subscribe'] = Input::post('subscribe') ? '1' : '0';
$post['stick_topic'] = Input::post('stick_topic') && $isAdmmod ? '1' : '0';
$post['message'] = Utils::linebreaks(Utils::trim(Input::post('req_message')));
// Validate BBCode syntax
if (ForumSettings::get('p_message_bbcode') == '1') {
$post['message'] = Parser::preparseBbcode($post['message'], $errors);
}
// Replace four-byte characters (MySQL cannot handle them)
$post['message'] = Utils::stripBadMultibyteChars($post['message']);
$post['time'] = time();
$post = Hooks::fire('model.post.setup_variables', $post);
return $post;
}
// If the previous check went OK, setup some variables used later
public static function setupEditVariables($curPost, $isAdmmod, $canEditSubject, $errors)
{
Hooks::fire('model.post.setup_edit_variables_start');
$post = [];
$post['hide_smilies'] = Input::post('hide_smilies') ? '1' : '0';
$post['stick_topic'] = Input::post('stick_topic') ? '1' : '0';
if (!$isAdmmod) {
$post['stick_topic'] = $curPost['sticky'];
}
// Clean up message from POST
$post['message'] = Utils::linebreaks(Utils::trim(Input::post('req_message')));
// Validate BBCode syntax
if (ForumSettings::get('p_message_bbcode') == '1') {
$post['message'] = Parser::preparseBbcode($post['message'], $errors);
}
// Replace four-byte characters (MySQL cannot handle them)
$post['message'] = Utils::stripBadMultibyteChars($post['message']);
// Get the subject
if ($canEditSubject) {
$post['subject'] = Utils::trim(Input::post('req_subject'));
}
$post = Hooks::fire('model.post.setup_edit_variables', $post);
return $post;
}
public function getInfoDelete($id)
{
$id = Hooks::fire('model.post.get_info_delete_start', $id);
$query['select'] = ['fid' => 'f.id', 'f.forum_name', 'f.moderators', 'f.redirect_url', 'fp.post_replies', 'fp.post_topics', 'tid' => 't.id', 't.subject', 't.first_post_id', 't.closed', 'p.poster', 'p.posted', 'p.poster_id', 'p.message', 'p.hide_smilies'];
$query['where'] = [
['fp.read_forum' => 'IS NULL'],
['fp.read_forum' => '1']
];
$query = DB::table('posts')
->tableAlias('p')
->selectMany($query['select'])
->innerJoin('topics', ['t.id', '=', 'p.topic_id'], 't')
->innerJoin('forums', ['f.id', '=', 't.forum_id'], 'f')
->leftOuterJoin('forum_perms', 'fp.forum_id=f.id AND fp.group_id='.User::get()->g_id, 'fp')
->whereAnyIs($query['where'])
->where('p.id', $id);
$query = Hooks::fireDB('model.post.get_info_delete_query', $query);
$query = $query->findOne();
if (!$query) {
throw new Error(__('Bad request'), 404);
}
return $query;
}
public static function handleDeletion($isTopicPost, $id, $curPost)
{
Hooks::fire('model.post.handle_deletion_start', $isTopicPost, $id, $curPost);
$tid = $curPost['tid'];
$fid = $curPost['fid'];
$topicSubject = Url::slug($curPost['subject']);
$forumUrl = Url::slug($curPost['forum_name']);
if ($isTopicPost) {
Hooks::fire('model.post.model.topic.delete', $isTopicPost, $id, $curPost);
// Delete the topic and all of its posts
Topic::delete($tid);
Forum::update($fid);
return Router::redirect(Router::pathFor('Forum', ['id' => $fid, 'name' => $forumUrl]), __('Topic del redirect'));
} else {
Hooks::fire('model.post.handle_deletion', $isTopicPost, $id, $curPost);
// Delete just this one post
self::delete($id, $tid);
Forum::update($fid);
// Redirect towards the previous post
$post = DB::table('posts')
->select('id')
->where('topic_id', $tid)
->whereLt('id', $id)
->orderByDesc('id');
$post = Hooks::fireDB('model.post.handle_deletion_query', $post);
$post = $post->findOne();
return Router::redirect(Router::pathFor('viewPost', ['id' => $tid, 'name' => $topicSubject, 'pid' => $post['id']]).'#p'.$post['id'], __('Post del redirect'));
}
}
//
// Delete a single post
//
public static function delete($postId, $topicId)
{
$result = DB::table('posts')
->selectMany('id', 'poster', 'posted')
->where('topic_id', $topicId)
->orderByDesc('id')
->limit(2)
->findMany();
$i = 0;
foreach ($result as $curResult) {
if ($i == 0) {
$lastId = $curResult['id'];
} else {
$secondLastId = $curResult['id'];
$secondPoster = $curResult['poster'];
$secondPosted = $curResult['posted'];
}
++$i;
}
// Delete the post
DB::table('posts')
->where('id', $postId)
->findOne()
->delete();
$search = new \FeatherBB\Core\Search();
$search->stripSearchIndex($postId);
// Count number of replies in the topic
$numReplies = DB::table('posts')->where('topic_id', $topicId)->count() - 1;
// If the message we deleted is the most recent in the topic (at the end of the topic)
if ($lastId == $postId) {
// If there is a $secondLastId there is more than 1 reply to the topic
if (isset($secondLastId)) {
$updateTopic = [
'last_post' => $secondPosted,
'last_post_id' => $secondLastId,
'last_poster' => $secondPoster,
'num_replies' => $numReplies,
];
DB::table('topics')
->where('id', $topicId)
->findOne()
->set($updateTopic)
->save();
} else {
// We deleted the only reply, so now last_post/last_post_id/last_poster is posted/id/poster from the topic itself
DB::table('topics')
->where('id', $topicId)
->findOne()
->setExpr('last_post', 'posted')
->setExpr('last_post_id', 'id')
->setExpr('last_poster', 'poster')
->set('num_replies', $numReplies)
->save();
}
} else {
// Otherwise we just decrement the reply counter
DB::table('topics')
->where('id', $topicId)
->findOne()
->set('num_replies', $numReplies)
->save();
}
}
public static function editPost($id, $canEditSubject, $post, $curPost, $isAdmmod, $username = null)
{
Hooks::fire('model.post.edit_post_start');
if ($canEditSubject) {
// Update the topic and any redirect topics
$whereTopic = [
['id' => $curPost['tid']],
['moved_to' => $curPost['tid']]
];
$query['update_topic'] = [
'subject' => $post['subject'],
'sticky' => $post['stick_topic']
];
$query = DB::table('topics')->whereAnyIs($whereTopic)
->findOne()
->set($query['update_topic']);
$query = Hooks::fireDB('model.post.edit_post_can_edit_subject', $query);
$query = $query->save();
// We changed the subject, so we need to take that into account when we update the search words
\FeatherBB\Core\Search::updateSearchIndex('edit', $id, $post['message'], $post['subject']);
} else {
\FeatherBB\Core\Search::updateSearchIndex('edit', $id, $post['message']);
}
// Update the post
unset($query);
$query['update_post'] = [
'message' => $post['message'],
'hide_smilies' => $post['hide_smilies']
];
if (!Input::post('silent') || !$isAdmmod) {
$query['update_post']['edited'] = time();
$query['update_post']['edited_by'] = (is_null($username) ? User::get()->username : $username);
}
$query = DB::table('posts')->where('id', $id)
->findOne()
->set($query['update_post']);
$query = Hooks::fireDB('model.post.edit_post_query', $query);
$query = $query->save();
}
public function report($postId)
{
$postId = Hooks::fire('model.post.insert_report_start', $postId);
// Clean up reason from POST
$reason = Utils::linebreaks(Utils::trim(Input::post('req_reason')));
if ($reason == '') {
throw new Error(__('No reason'), 400);
} elseif (strlen($reason) > 65535) { // TEXT field can only hold 65535 bytes
throw new Error(__('Reason too long'), 400);
}
if (User::get()->last_report_sent != '' && (time() - User::get()->last_report_sent) < User::getPref('report.min_interval') && (time() - User::get()->last_report_sent) >= 0) {
throw new Error(sprintf(__('Report flood'), User::getPref('report.min_interval'), User::getPref('report.min_interval') - (time() - User::get()->last_report_sent)), 429);
}
// Get the topic ID
$topic = DB::table('posts')->select('topic_id')
->where('id', $postId);
$topic = Hooks::fireDB('model.post.insert_report_topic_id', $topic);
$topic = $topic->findOne();
if (!$topic) {
throw new Error(__('Bad request'), 404);
}
// Get the subject and forum ID
$report['select'] = ['subject', 'forum_id'];
$report = DB::table('topics')->selectMany($report['select'])
->where('id', $topic['topic_id']);
$report = Hooks::fireDB('model.post.insert_report_get_subject', $report);
$report = $report->findOne();
if (!$report) {
throw new Error(__('Bad request'), 404);
}
// Should we use the internal report handling?
if (ForumSettings::get('o_report_method') == '0' || ForumSettings::get('o_report_method') == '2') {
// Insert the report
$query['insert'] = [
'post_id' => $postId,
'topic_id' => $topic['topic_id'],
'forum_id' => $report['forum_id'],
'reported_by' => User::get()->id,
'created' => time(),
'message' => $reason,
];
$query = DB::table('reports')
->create()
->set($query['insert']);
$query = Hooks::fireDB('model.post.insert_report_query', $query);
$query = $query->save();
}
// Should we email the report?
if (ForumSettings::get('o_report_method') == '1' || ForumSettings::get('o_report_method') == '2') {
// We send it to the complete mailing-list in one swoop
if (ForumSettings::get('o_mailing_list') != '') {
// Load the "new report" template
$mailTpl = trim(file_get_contents(ForumEnv::get('FEATHER_ROOT').'featherbb/lang/'.User::getPref('language').'/mail_templates/new_report.tpl'));
$mailTpl = Hooks::fire('model.post.insert_report_mail_tpl', $mailTpl);
// The first row contains the subject
$firstCrlf = strpos($mailTpl, "\n");
$mailSubject = trim(substr($mailTpl, 8, $firstCrlf-8));
$mailMessage = trim(substr($mailTpl, $firstCrlf));
$mailSubject = str_replace('
'.sprintf(__('Host info 2'), @gethostbyaddr($ip)).'
'.__('Show more users').'', 400, true, true);
}
}