Skip to content

Instantly share code, notes, and snippets.

@joshuaadickerson
Last active August 29, 2015 14:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joshuaadickerson/fea8b2dd1a5ca756f6a0 to your computer and use it in GitHub Desktop.
Save joshuaadickerson/fea8b2dd1a5ca756f6a0 to your computer and use it in GitHub Desktop.
Working on refactoring. Taking a while and a lot of work
<?php
/**
* The job of this file is to handle everything related to posting replies,
* new topics, quotes, and modifications to existing posts. It also handles
* quoting posts by way of javascript.
*
* @name ElkArte Forum
* @copyright ElkArte Forum contributors
* @license BSD http://opensource.org/licenses/BSD-3-Clause
*
* This software is a derived product, based on:
*
* Simple Machines Forum (SMF)
* copyright: 2011 Simple Machines (http://www.simplemachines.org)
* license: BSD, See included LICENSE.TXT for terms and conditions.
*
* @version 1.1 dev
*
*/
if (!defined('ELK'))
die('No access...');
/**
* Post Controller
*/
class Post_Controller extends Action_Controller
{
/**
* The post (messages) errors object
* @var null|object
*/
protected $_post_errors = null;
/**
* The template layers object
* @var null|object
*/
protected $_template_layers = null;
/**
* An array of attributes of the topic (if not new)
* @var mixed[]
*/
protected $_topic_attributes = array(
'locked' => false,
'notify' => false,
'is_sticky' => false,
'id_last_msg' => 0,
'id_member' => 0,
'id_first_msg' => 0,
'subject' => '',
'last_post_time' => 0
);
protected $msg_info = array();
protected $msg_id;
protected $topic_info = array();
protected $topic_id;
protected $becomes_approved = true;
protected $max_subject_length = 100;
protected $previewing = false;
/**
* Sets up common stuff for all or most of the actions.
*/
public function pre_dispatch()
{
global $context;
$this->_post_errors = Error_Context::context('post', 1);
$this->_template_layers = Template_Layers::getInstance();
require_once(SUBSDIR . '/Post.subs.php');
require_once(SUBSDIR . '/Messages.subs.php');
require_once(SUBSDIR . '/Topic.subs.php');
$this->topic_id = isset($GLOBALS['topic']) ?: (int) $GLOBALS['topic'];
$this->msg_id = empty($_REQUEST['msg']) ? 0 : (int) $_REQUEST['msg'];
// No need!
$context['robot_no_index'] = true;
}
/**
* Dispatch to the right action method for the request.
*
* @see Action_Controller::action_index()
*/
public function action_index()
{
// Figure out the right action to do.
// hint: I'm post controller. :P
$this->action_post();
}
/**
* Handles showing the post screen, loading the post to be modified, and loading any post quoted.
*
* - additionally handles previews of posts.
* - requires different permissions depending on the actions, but most notably post_new, post_reply_own, and post_reply_any.
* - shows options for the editing and posting of calendar events and attachments, as well as the posting of polls (using modules).
* - accessed from ?action=post.
*
* @uses the Post template and language file, main sub template.
*/
public function action_post()
{
global $txt, $modSettings, $board, $user_info, $context;
loadLanguage('Post');
loadLanguage('Errors');
$this->_template_layers->add('postarea');
$this->_events->trigger('prepare_post', array('topic_attributes' => &$this->_topic_attributes));
// You must be posting to *some* board.
if (empty($board) && !$context['make_event'])
{
Errors::instance()->fatal_lang_error('no_board', false);
}
// All those wonderful modifiers and attachments
$this->_template_layers->add('additional_options', 200);
// @todo should this moved to a separate function and be done in pre_dispatch()?
// No message is complete without a topic.
if (empty($this->topic_id) && !empty($this->msg_id))
{
$this->topic_id = associatedTopic($this->msg_id);
if (empty($this->topic_id))
{
unset($_REQUEST['msg'], $_POST['msg'], $_GET['msg']);
}
}
$context['becomes_approved'] = $this->setBecomesApproved();
// Check if it's locked. It isn't locked if no topic is specified.
if (!$this->isNewTopic())
{
if (empty($this->msg_id) && $user_info['is_guest'] && !allowedTo('post_reply_any') && (!$modSettings['postmod_active'] || !allowedTo('post_unapproved_replies_any')))
{
is_not_guest();
}
$this->_topic_attributes = topicUserAttributes($this->topic_id, $user_info['id']);
$context['notify'] = $this->_topic_attributes['notify'];
$context['topic_last_message'] = $this->_topic_attributes['id_last_msg'];
$context['can_lock'] = allowedTo('lock_any') || ($user_info['id'] == $this->_topic_attributes['id_member'] && allowedTo('lock_own'));
}
else
{
if ((!$context['make_event'] || !empty($board)) && !($modSettings['postmod_active'] && !allowedTo('post_new') && allowedTo('post_unapproved_topics')))
{
isAllowedTo('post_new');
}
// @todo These won't work if you're making an event.
$context['can_lock'] = allowedTo(array('lock_any', 'lock_own'));
}
// Don't allow a post if it's locked and you aren't all powerful.
if ($this->_topic_attributes['locked'] && !allowedTo('moderate_board'))
{
Errors::instance()->fatal_lang_error('topic_locked', false);
}
try
{
$this->_events->trigger('prepare_context', array('id_member_poster' => $this->_topic_attributes['id_member']));
}
catch (Controller_Redirect_Exception $e)
{
return $e->doRedirect($this);
}
// See if any new replies have come along.
if ($this->isNewReply() && $this->hasNewRepliesSincePosting())
{
$this->newRepliesContext();
}
$context['destination'] = 'post2;start=' . $_REQUEST['start'];
// Previewing, modifying, or posting?
// Do we have a body, but an error happened.
if (isset($_REQUEST['message']) || $this->_post_errors->hasErrors())
{
// Set up the inputs for the form.
$form_subject = $this->getSubject(false);
$form_message = $this->getBody(false);
// Validate inputs.
if (!$this->_post_errors->hasErrors())
{
// This means they didn't click Post and get an error.
$this->previewing = true;
}
else
{
// They are previewing if they asked to preview (i.e. came from quick reply).
$this->previewing = !empty($_REQUEST['preview']) || isset($_REQUEST['xml']);
}
$this->setupPreview();
if (!$this->isModifyingMessage() && isset($_REQUEST['last_msg']))
{
list ($form_subject,) = getFormMsgSubject($this->getEditingCase(), $this->topic_id, $this->_topic_attributes['subject']);
}
}
// Editing a message...
elseif ($this->isModifyingMessage())
{
$message = getFormMsgSubject(1, $this->topic_id, '', $this->msg_id);
// The message they were trying to edit was most likely deleted.
if ($message === false)
{
Errors::instance()->fatal_lang_error('no_message', false);
}
$this->_events->trigger('prepare_editing', array('topic' => $this->topic_id, 'message' => &$message));
if (!empty($message['errors']))
{
foreach ($message['errors'] as $error)
{
$this->_post_errors->addError($error);
}
}
censorText($message['message']['subject']);
censorText(un_preparsecode($message['message']['body']));
// Get the stuff ready for the form.
$form_subject = $message['message']['subject'];
$form_message = $message['message']['body'];
// Check the boxes that should be checked.
$context['use_smileys'] = !empty($message['message']['smileys_enabled']);
$context['icon'] = $message['message']['icon'];
// Set the destination.
$context['destination'] .= ';msg=' . $this->msg_id . ';' . $context['session_var'] . '=' . $context['session_id'];
$context['submit_label'] = $txt['save'];
}
// Posting...
else
{
// By default....
$context['use_smileys'] = true;
if ($user_info['is_guest'])
{
$context['name'] = isset($_SESSION['guest_name']) ? $_SESSION['guest_name'] : '';
$context['email'] = isset($_SESSION['guest_email']) ? $_SESSION['guest_email'] : '';
}
$this->_events->trigger('prepare_posting');
$context['submit_label'] = $txt['post'];
list ($form_subject, $form_message) = getFormMsgSubject($this->getEditingCase(), $this->topic_id, $this->_topic_attributes['subject']);
}
// Check whether this is a really old post being bumped...
$this->isOldPost();
$this->_events->trigger('post_errors');
// Any errors occurred?
$context['post_error'] = array(
'errors' => $this->_post_errors->prepareErrors(),
'type' => $this->_post_errors->getErrorType() == 0 ? 'minor' : 'serious',
'title' => $this->_post_errors->getErrorType() == 0 ? $txt['warning_while_submitting'] : $txt['error_while_submitting'],
);
// Update the topic summary, needed to show new posts in a preview
if (!$this->isNewTopic() && !empty($modSettings['topicSummaryPosts']))
{
$this->getPreviousPosts();
}
// Just ajax previewing then lets stop now
if (isset($_REQUEST['xml']))
{
$context['sub_template'] = 'post';
// Just in case of an earlier error...
$context['preview_message'] = '';
$context['preview_subject'] = '';
obExit();
}
$this->postPageContext($form_subject, $form_message);
// Register this form in the session variables.
checkSubmitOnce('register');
// Finally, load the template.
loadTemplate('Post');
$context['sub_template'] = 'post_page';
}
/**
* Posts or saves the message composed with Post().
*
* requires various permissions depending on the action.
* handles attachment, post, and calendar saving.
* sends off notifications, and allows for announcements and moderation.
* accessed from ?action=post2.
*/
public function action_post2()
{
global $board, $context, $user_settings;
global $user_info, $board_info, $modSettings;
// Sneaking off, are we?
if (empty($_POST) && $this->isNewTopic())
{
if (empty($_SERVER['CONTENT_LENGTH']))
{
redirectexit('action=post;board=' . $board . '.0');
}
else
{
Errors::instance()->fatal_lang_error('post_upload_error', false);
}
}
elseif (empty($_POST) && !$this->isNewTopic())
{
redirectexit('action=post;topic=' . $this->topic_id . '.0');
}
// We are now in post2 action
$context['current_action'] = 'post2';
// If the session has timed out, let the user re-submit their form.
if (checkSession('post', '', false) != '')
{
$this->_post_errors->addError('session_timeout');
// Disable the preview so that any potentially malicious code is not executed
$_REQUEST['preview'] = false;
return $this->action_post();
}
// Previewing? Go back to start.
if (isset($_REQUEST['preview']))
{
return $this->action_post();
}
require_once(SUBSDIR . '/Boards.subs.php');
loadLanguage('Post');
// Prevent double submission of this form.
checkSubmitOnce('check');
// If this isn't a new topic load the topic info that we need.
if (!$this->isNewTopic())
{
$this->getTopicInfo();
if (!empty($this->topic_info['locked']) && !allowedTo('moderate_board'))
{
Errors::instance()->fatal_lang_error('topic_locked', false);
}
// Replying to a topic?
if ($this->isNewReply())
{
$this->newReply();
}
// Modifying an existing message.
elseif ($this->isModifyingMessage())
{
$this->msg_info = basicMessageInfo($this->msg_id, true);
if (empty($this->msg_info))
{
Errors::instance()->fatal_lang_error('cant_find_messages', false);
}
$this->modifyMessage();
}
}
// Posting a new topic.
else
{
$this->newTopic();
// If the number of replies has changed, if the setting is enabled, go back to action_post() - which handles the error.
if ($this->hasNewRepliesSincePosting())
{
return $this->action_post();
}
}
// If the poster is a guest evaluate the legality of name and email.
if ($this->posterIsGuest())
{
$email = $this->getGuestEmail();
$guestname = $this->getGuestName();
}
try
{
$this->_events->trigger('before_save_post', array('post_errors' => $this->_post_errors, 'topic_info' => &$this->topic_info));
}
catch (Controller_Redirect_Exception $e)
{
return $e->doRedirect($this);
}
// Check the subject and message.
$subject = $this->getSubject();
$body = $this->getBody();
// if new message
if (empty($this->msg_id))
{
$guestname = $user_info['username'];
$email = $user_info['email'];
}
// Posting somewhere else? Are we sure you can?
if (!empty($_REQUEST['post_in_board']))
{
$new_board = (int) $_REQUEST['post_in_board'];
if (!allowedTo('post_new', $new_board))
{
$post_in_board = boardInfo($new_board);
if (!empty($post_in_board))
{
$this->_post_errors->addError(array('post_new_board', array($post_in_board['name'])));
}
else
{
$this->_post_errors->addError('post_new');
}
}
}
// Any mistakes?
if ($this->_post_errors->hasErrors())
{
return $this->action_post();
}
// Make sure the user isn't spamming the board.
if (!$this->isModifyingMessage())
{
spamProtection('post');
}
// At about this point, we're posting and that's that.
ignore_user_abort(true);
setTimeLimit(300);
$msgOptions = $this->getMessageOptions($subject, $body);
$topicOptions = $this->getTopicOptions(true);
$posterOptions = $this->getPosterOptions($guestname, $email);
// @todo move this up higher so the get*Options() requests can load everything?
// We also have to fake the board:
// if it's valid and it's not the current, let's forget about the "current" and load the new one
if (empty($this->msg_id) && !empty($_REQUEST['post_in_board']))
{
$new_board = (int) $_REQUEST['post_in_board'];
if (!empty($new_board) && $board !== $new_board)
{
$board = $new_board;
loadBoard();
// Some details changed
$topicOptions['board'] = $board;
$topicOptions['is_approved'] = !$modSettings['postmod_active'] || empty($this->topic_id) || !empty($board_info['cur_topic_approved']);
$posterOptions['update_post_count'] = !$user_info['is_guest'] && empty($this->msg_id) && $board_info['posts_count'];
}
}
$this->savePost($msgOptions, $topicOptions, $posterOptions);
// Marking boards as read.
// (You just posted and they will be unread.)
if (!$user_info['is_guest'])
{
$board_list = !empty($board_info['parent_boards']) ? array_keys($board_info['parent_boards']) : array();
// Returning to the topic?
if (!empty($_REQUEST['goback']))
{
$board_list[] = $board;
}
if (!empty($board_list))
{
markBoardsRead($board_list, false, false);
}
}
// Turn notification on or off.
if (!empty($_POST['notify']) && allowedTo('mark_any_notify'))
{
setTopicNotification($user_info['id'], $this->topic_id, true);
}
elseif (!$this->isNewTopic())
{
setTopicNotification($user_info['id'], $this->topic_id, false);
}
// Log an act of moderation - modifying.
if (!empty($this->moderationAction))
{
logAction('modify', array('topic' => $this->topic_id, 'message' => (int) $this->msg_id, 'member' => $this->msg_info['id_member'], 'board' => $board));
}
if (isset($_POST['lock']) && $_POST['lock'] != 2)
{
logAction(empty($_POST['lock']) ? 'unlock' : 'lock', array('topic' => $topicOptions['id'], 'board' => $topicOptions['board']));
}
if (isset($_POST['sticky']))
{
logAction(empty($_POST['sticky']) ? 'unsticky' : 'sticky', array('topic' => $topicOptions['id'], 'board' => $topicOptions['board']));
}
// Notify any members who have notification turned on for this topic/board - only do this if it's going to be approved(!)
if ($this->becomes_approved)
{
require_once(SUBSDIR . '/Notification.subs.php');
if ($this->isNewTopic())
{
$notifyData = array(
'body' => $body,
'subject' => $subject,
'name' => $user_info['name'],
'poster' => $user_info['id'],
'msg' => $msgOptions['id'],
'board' => $board,
'topic' => $this->topic_id,
'signature' => (isset($user_settings['signature']) ? $user_settings['signature'] : ''),
);
sendBoardNotifications($notifyData);
}
elseif (empty($this->msg_id))
{
// Only send it to everyone if the topic is approved, otherwise just to the topic starter if they want it.
if ($this->topic_info['approved'])
{
sendNotifications($this->topic_id, 'reply');
}
else
{
sendNotifications($this->topic_id, 'reply', array(), $this->topic_info['id_member_started']);
}
}
}
if ($board_info['num_topics'] == 0)
{
cache_put_data('board-' . $board, null, 120);
}
// Now, where do we exit to?
if (!empty($_POST['announce_topic']))
{
redirectexit('action=announce;sa=selectgroup;topic=' . $this->topic_id . (!empty($_POST['move']) && allowedTo('move_any') ? ';move' : '') . (empty($_REQUEST['goback']) ? '' : ';goback'));
}
if (!empty($_POST['move']) && allowedTo('move_any'))
{
redirectexit('action=movetopic;topic=' . $this->topic_id . '.0' . (empty($_REQUEST['goback']) ? '' : ';goback'));
}
// Return to post if the mod is on.
if (isset($this->msg_id) && !empty($_REQUEST['goback']))
{
redirectexit('topic=' . $this->topic_id . '.msg' . $this->msg_id . '#msg' . $this->msg_id, isBrowser('ie'));
}
elseif (!empty($_REQUEST['goback']))
{
redirectexit('topic=' . $this->topic_id . '.new#new', isBrowser('ie'));
}
// Dut-dut-duh-duh-DUH-duh-dut-duh-duh! *dances to the Final Fantasy Fanfare...*
else
{
redirectexit('board=' . $board . '.0');
}
}
/**
* Loads a post and inserts it into the current editing text box.
* Used to quick edit a post as well as to quote a post and place it in the quick reply box
* Can be used to quick edit just the subject from the topic listing
*
* uses the Post language file.
* uses special (sadly browser dependent) javascript to parse entities for internationalization reasons.
* accessed with ?action=quotefast and ?action=quotefast;modify
*/
public function action_quotefast()
{
global $user_info, $context;
loadLanguage('Post');
// @todo check that $_GET['quote'] exists
// Where we going if we need to?
$context['post_box_name'] = isset($_GET['pb']) ? $_GET['pb'] : '';
$row = quoteMessageInfo((int) $_GET['quote'], isset($_GET['modify']));
$context['sub_template'] = 'quotefast';
if (!empty($row))
{
$can_view_post = $row['approved'] || ($row['id_member'] != 0 && $row['id_member'] == $user_info['id']) || allowedTo('approve_posts', $row['id_board']);
}
if (!empty($can_view_post))
{
// Remove special formatting we don't want anymore.
$row['body'] = un_preparsecode($row['body']);
// Censor the message!
censorText($row['body']);
$row['body'] = preg_replace('~<br ?/?' . '>~i', "\n", $row['body']);
// Want to modify a single message by double clicking it?
if (isset($_GET['modify']))
{
censorText($row['subject']);
$context['sub_template'] = 'modifyfast';
$context['message'] = array(
'id' => $_GET['quote'],
'body' => $row['body'],
'subject' => addcslashes($row['subject'], '"'),
);
return;
}
// Remove any nested quotes.
$row['body'] = $this->removeNestedQuotes($row['body']);
// Add a quote string on the front and end.
$context['quote']['xml'] = '[quote author=' . $row['poster_name'] . ' link=msg=' . (int) $_GET['quote'] . ' date=' . $row['poster_time'] . "]\n" . $row['body'] . "\n[/quote]";
$context['quote']['text'] = strtr(un_htmlspecialchars($context['quote']['xml']), array('\'' => '\\\'', '\\' => '\\\\', "\n" => '\\n', '</script>' => '</\' + \'script>'));
$context['quote']['xml'] = strtr($context['quote']['xml'], array('&nbsp;' => '&#160;', '<' => '&lt;', '>' => '&gt;'));
$context['quote']['mozilla'] = strtr(Util::htmlspecialchars($context['quote']['text']), array('&quot;' => '"'));
}
//@todo Needs a nicer interface.
// In case our message has been removed in the meantime.
elseif (isset($_GET['modify']))
{
$context['sub_template'] = 'modifyfast';
$context['message'] = array(
'id' => 0,
'body' => '',
'subject' => '',
);
}
else
{
$context['quote'] = array(
'xml' => '',
'mozilla' => '',
'text' => '',
);
}
}
/**
* Used to edit the body or subject of a message inline
* called from action=jsmodify from script and topic js
*/
public function action_jsmodify()
{
global $board, $user_info, $context;
// We have to have a topic!
if (empty($this->topic_id))
{
obExit(false);
}
checkSession('get');
$this->topic_info = getTopicInfoByMsg($this->topic_id, empty($this->msg_id) ? 0 : (int) $this->msg_id);
if (empty($this->topic_info))
{
Errors::instance()->fatal_lang_error('no_board', false);
}
// Since getTopicInfoByMsg() has all of the msg_info in it, this saves space.
$this->msg_info = $this->topic_info;
// Change either body or subject requires permissions to modify messages.
if (isset($_POST['message']) || isset($_POST['subject']) || isset($_REQUEST['icon']))
{
$this->modifyMessage();
}
$subject = $this->getSubject();
$body = $this->getBody();
if (!$this->_post_errors->hasErrors())
{
$msgOptions = $this->getMessageOptions($subject, $body);
$topicOptions = $this->getTopicOptions(false);
$posterOptions = array();
// Only consider marking as editing if they have edited the subject, message or icon.
if ((!empty($subject) && $subject != $this->topic_info['subject']) || (!empty($body) && $body != $this->topic_info['body']) || (isset($_REQUEST['icon']) && $_REQUEST['icon'] != $this->topic_info['icon']))
{
// // And even then only if the time has passed...
// if (time() - $this->topic_info['poster_time'] > $modSettings['edit_wait_time'] || $user_info['id'] != $this->topic_info['id_member'])
// {
// $msgOptions['modify_time'] = time();
// $msgOptions['modify_name'] = $user_info['name'];
// }
}
// If nothing was changed there's no need to add an entry to the moderation log.
if (!(!empty($subject) && $subject != $this->topic_info['subject']) || (!empty($body) && $body != $this->topic_info['body']) || (isset($_REQUEST['icon']) && $_REQUEST['icon'] != $this->topic_info['icon']))
{
$this->moderationAction = false;
}
modifyPost($msgOptions, $topicOptions, $posterOptions);
// If we didn't change anything this time but had before put back the old info.
if (!isset($msgOptions['modify_time']) && !empty($this->topic_info['modified_time']))
{
$msgOptions['modify_time'] = $this->topic_info['modified_time'];
$msgOptions['modify_name'] = $this->topic_info['modified_name'];
}
// Changing the first subject updates other subjects to 'Re: new_subject'.
if (!empty($subject) && isset($_REQUEST['change_all_subjects']) && $this->topic_info['id_first_msg'] == $this->topic_info['id_msg'] && !empty($this->topic_info['num_replies']) && (allowedTo('modify_any') || ($this->topic_info['id_member_started'] == $user_info['id'] && allowedTo('modify_replies'))))
{
// Get the proper (default language) response prefix first.
$context['response_prefix'] = response_prefix();
topicSubject(array('id_topic' => $this->topic_id, 'id_first_msg' => $this->topic_info['id_first_msg']), $subject, $context['response_prefix'], true);
}
if (!empty($this->moderationAction))
{
logAction('modify', array('topic' => $this->topic_id, 'message' => $this->topic_info['id_msg'], 'member' => $this->topic_info['id_member'], 'board' => $board));
}
}
if (isset($_REQUEST['xml']))
{
$this->jsmodifyXMLContext($msgOptions);
}
else
{
obExit(false);
}
}
protected function jsmodifyXMLContext($msgOptions)
{
$context['sub_template'] = 'modifydone';
if (!$this->_post_errors->hasErrors() && isset($msgOptions['subject']) && isset($msgOptions['body']))
{
$context['message'] = array(
'id' => $this->topic_info['id_msg'],
'modified' => array(
'time' => isset($msgOptions['modify_time']) ? standardTime($msgOptions['modify_time']) : '',
'html_time' => isset($msgOptions['modify_time']) ? htmlTime($msgOptions['modify_time']) : '',
'timestamp' => isset($msgOptions['modify_time']) ? forum_time(true, $msgOptions['modify_time']) : 0,
'name' => isset($msgOptions['modify_time']) ? $msgOptions['modify_name'] : '',
),
'subject' => $msgOptions['subject'],
'first_in_topic' => $this->topic_info['id_msg'] == $this->topic_info['id_first_msg'],
'body' => strtr($msgOptions['body'], array(']]>' => ']]]]><![CDATA[>')),
);
censorText($context['message']['subject']);
censorText($context['message']['body']);
$context['message']['body'] = parse_bbc($context['message']['body'], $this->topic_info['smileys_enabled'], $this->topic_info['id_msg']);
}
// Topic?
elseif (!$this->_post_errors->hasErrors())
{
$context['sub_template'] = 'modifytopicdone';
$context['message'] = array(
'id' => $this->topic_info['id_msg'],
'modified' => array(
'time' => isset($msgOptions['modify_time']) ? standardTime($msgOptions['modify_time']) : '',
'html_time' => isset($msgOptions['modify_time']) ? htmlTime($msgOptions['modify_time']) : '',
'timestamp' => isset($msgOptions['modify_time']) ? forum_time(true, $msgOptions['modify_time']) : 0,
'name' => isset($msgOptions['modify_time']) ? $msgOptions['modify_name'] : '',
),
'subject' => isset($msgOptions['subject']) ? $msgOptions['subject'] : '',
);
censorText($context['message']['subject']);
}
else
{
$context['message'] = array(
'id' => $this->topic_info['id_msg'],
'errors' => array(),
'error_in_subject' => $this->_post_errors->hasError('no_subject'),
'error_in_body' => $this->_post_errors->hasError('no_message') || $this->_post_errors->hasError('long_message'),
);
$context['message']['errors'] = $this->_post_errors->prepareErrors();
}
}
protected function _checkLocked($lock)
{
global $user_info;
// A new topic
if (empty($this->topic_info))
{
// New topics are by default not locked.
if (empty($lock))
{
return;
}
// Besides, you need permission.
elseif (!allowedTo(array('lock_any', 'lock_own')))
{
return;
}
// A moderator-lock (1) can override a user-lock (2).
else
{
return allowedTo('lock_any') ? 1 : 2;
}
}
// Nothing changes to the lock status.
if ((empty($lock) && empty($this->topic_info['locked'])) || (!empty($lock) && !empty($this->topic_info['locked'])))
{
return;
}
// You're simply not allowed to (un)lock this.
elseif (!allowedTo(array('lock_any', 'lock_own')) || (!allowedTo('lock_any') && $user_info['id'] != $this->topic_info['id_member_started']))
{
return;
}
// You're only allowed to lock your own topics.
elseif (!allowedTo('lock_any'))
{
// You're not allowed to break a moderator's lock.
if ($this->topic_info['locked'] == 1)
{
return;
}
// Lock it with a soft lock or unlock it.
else
{
$lock = empty($lock) ? 0 : 2;
}
}
// You must be the moderator.
else
{
$lock = empty($lock) ? 0 : 1;
}
return $lock;
}
protected function newTopic()
{
// Now don't be silly, new topics will get their own id_msg soon enough.
unset($_REQUEST['msg'], $_POST['msg'], $_GET['msg']);
// Do like, the permissions, for safety and stuff...
$this->setBecomesApproved();
$this->_events->trigger('save_new_topic', array('becomes_approved' => &$this->becomes_approved));
}
protected function newReply()
{
$this->setBecomesApproved();
// So you wanna (un)sticky this...let's see.
if (isset($_POST['sticky']) && ($_POST['sticky'] == $this->topic_info['is_sticky'] || !allowedTo('make_sticky')))
{
unset($_POST['sticky']);
}
$this->_events->trigger('save_replying', array('topic_info' => &$this->topic_info));
}
protected function modifyMessage()
{
global $modSettings, $user_info;
$this->_events->trigger('save_modify', array('msgInfo' => &$this->msg_info));
if ($this->msg_info['id_member'] == $user_info['id'] && !allowedTo('modify_any'))
{
if ((!$modSettings['postmod_active'] || $this->msg_info['approved']) && !empty($modSettings['edit_disable_time']) && $this->msg_info['poster_time'] + ($modSettings['edit_disable_time'] + 5) * 60 < time())
{
Errors::instance()->fatal_lang_error('modify_post_time_passed', false);
}
elseif ($this->topic_info['id_member_started'] == $user_info['id'] && !allowedTo('modify_own'))
{
isAllowedTo('modify_replies');
}
else
{
isAllowedTo('modify_own');
}
}
elseif ($this->topic_info['id_member_started'] == $user_info['id'] && !allowedTo('modify_any'))
{
isAllowedTo('modify_replies');
// If you're modifying a reply, I say it better be logged...
$this->moderationAction = true;
}
else
{
isAllowedTo('modify_any');
// Log it, assuming you're not modifying your own post.
if ($this->msg_info['id_member'] != $user_info['id'])
{
$this->moderationAction = true;
}
}
if (!allowedTo('moderate_forum') || !$this->posterIsGuest())
{
$_POST['guestname'] = $this->msg_info['poster_name'];
$_POST['email'] = $this->msg_info['poster_email'];
}
}
public function isNewTopic()
{
return empty($GLOBALS['topic']);
}
public function isNewReply()
{
return !$this->isNewTopic() && empty($this->msg_id);
}
public function isModifyingMessage()
{
return !empty($this->msg_id) && !$this->isNewTopic();
}
protected function getTopicInfo()
{
global $board;
$this->topic_info = getTopicInfo($this->topic_id);
$this->_events->trigger('prepare_save_post', array('topic_info' => &$this->topic_info));
// Though the topic should be there, it might have vanished.
if (empty($this->topic_info))
{
Errors::instance()->fatal_lang_error('topic_doesnt_exist');
}
// Did this topic suddenly move? Just checking...
if ($this->topic_info['id_board'] != $board)
{
Errors::instance()->fatal_lang_error('not_a_topic');
}
return $this->topic_info;
}
protected function getSubject($validate = true)
{
if ($validate && (!isset($_REQUEST['subject']) || Util::htmltrim(Util::htmlspecialchars($_REQUEST['subject'])) === ''))
{
$this->_post_errors->addError('no_subject');
}
else
{
$subject = !isset($_REQUEST['subject']) ? '' : $_REQUEST['subject'];
return $this->filterSubject($subject);
}
}
protected function filterSubject($subject)
{
$subject = strtr(Util::htmlspecialchars($subject), array("\r" => '', "\n" => '', "\t" => ''));
// Make sure the subject isn't too long - taking into account special characters.
if (Util::strlen($subject) > $this->max_subject_length)
{
$subject = Util::substr($subject, 0, $this->max_subject_length);
}
return $subject;
}
/**
*
* @return string
*/
protected function getBody($validate = false)
{
global $modSettings, $user_info;
if (!isset($_REQUEST['message']))
{
if ($validate)
{
$this->_post_errors->addError('no_message');
}
return '';
}
$body = Util::htmltrim(Util::htmlspecialchars($_REQUEST['message'], ENT_QUOTES));
if (!empty($modSettings['max_messageLength']) && Util::strlen($_REQUEST['message']) > $modSettings['max_messageLength'])
{
if ($validate)
{
$this->_post_errors->addError(array('long_message', array($modSettings['max_messageLength'])));
}
}
else
{
// Prepare the message a bit for some additional testing.
//$body = Util::htmlspecialchars($_REQUEST['message'], ENT_QUOTES);
// Preparse code. (Zef)
if ($user_info['is_guest'])
{
$user_info['name'] = $_REQUEST['guestname'];
}
preparsecode($body);
// Let's see if there's still some content left without the tags.
if ($validate && Util::htmltrim(strip_tags(parse_bbc($body, false), '<img>')) === '' && (!allowedTo('admin_forum') || strpos($body, '[html]') === false))
{
$this->_post_errors->addError('no_message');
}
}
return $body;
}
/**
*
* @return string
*/
protected function getGuestName($validate = true)
{
$guestname = !isset($_REQUEST['guestname']) ? '' : Util::htmlspecialchars(trim($_REQUEST['guestname']));
if ($validate)
{
$this->validateGuestName($guestname);
}
return $guestname;
}
protected function validateGuestName($guestname)
{
if ($guestname == '' || $guestname == '_')
{
$this->_post_errors->addError('no_name');
}
if (Util::strlen($guestname) > 25)
{
$this->_post_errors->addError('long_name');
}
// If user is a guest, make sure the chosen name isn't taken.
require_once(SUBSDIR . '/Members.subs.php');
if (isReservedName($guestname, 0, true, false) && (!isset($this->msg_info['poster_name']) || $guestname != $this->msg_info['poster_name']))
{
$this->_post_errors->addError('bad_name');
}
// In case they are making multiple posts this visit, help them along by storing their name.
if (!$this->_post_errors->hasErrors())
{
$_SESSION['guest_name'] = $guestname;
}
}
/**
*
* @return string
*/
public function getGuestEmail($validate = true)
{
global $modSettings, $txt;
$email = !isset($_REQUEST['email']) ? '' : Util::htmlspecialchars(trim($_REQUEST['email']));
if (!$validate)
{
return $email;
}
if (empty($modSettings['guest_post_no_email']))
{
// Only check if they changed it!
if (!isset($this->msg_info) || $this->msg_info['poster_email'] != $email)
{
if (!allowedTo('moderate_forum') && !Data_Validator::is_valid($_POST, array('email' => 'valid_email|required'), array('email' => 'trim')))
{
empty($email) ? $this->_post_errors->addError('no_email') : $this->_post_errors->addError('bad_email');
}
}
// Now make sure this email address is not banned from posting.
isBannedEmail($email, 'cannot_post', sprintf($txt['you_are_post_banned'], $txt['guest_title']));
}
if (!$this->_post_errors->hasErrors())
{
$_SESSION['guest_email'] = $email;
}
return $email;
}
/**
*
* @return bool
*/
protected function posterIsGuest()
{
global $user_info;
if ($this->isModifyingMessage())
{
$posterIsGuest = empty($this->msg_info['id_member']);
}
else
{
$posterIsGuest = $user_info['is_guest'];
}
return $posterIsGuest;
}
/**
*
* @return int
*/
protected function getEditingCase()
{
global $modSettings;
// @todo: sort out what kind of combinations are actually possible
// Posting a quoted reply?
if ((!empty($this->topic_id) && !empty($_REQUEST['quote'])) || (!empty($modSettings['enableFollowup']) && !empty($_REQUEST['followup'])))
{
$case = 2;
}
// Posting a reply without a quote?
elseif (!empty($this->topic_id) && empty($_REQUEST['quote']))
{
$case = 3;
}
else
{
$case = 4;
}
return $case;
}
protected function getMessageOptions($subject, $body)
{
global $modSettings, $user_info;
$msgOptions = array(
'id' => $this->msg_id,
'subject' => $subject,
'body' => $body,
'icon' => $this->getIcon(),
'smileys_enabled' => !isset($_POST['ns']),
'approved' => $this->becomes_approved,
);
if (!$this->isModifyingMessage())
{
// Have admins allowed people to hide their screwups?
if (time() - $this->msg_info['poster_time'] > $modSettings['edit_wait_time'] || $user_info['id'] != $this->msg_info['id_member'])
{
$msgOptions['modify_time'] = time();
$msgOptions['modify_name'] = $user_info['name'];
}
// If we didn't change anything this time but had before put back the old info.
elseif (!empty($this->topic_info['modified_time']))
{
$msgOptions['modify_time'] = $this->topic_info['modified_time'];
$msgOptions['modify_name'] = $this->topic_info['modified_name'];
}
// This will save some time...
if ($this->msg_info['approved'] != $this->becomes_approved)
{
unset($msgOptions['approved']);
}
}
return $msgOptions;
}
protected function getTopicOptions($mark_as_read)
{
global $board, $modSettings, $board_info;
$lock = isset($_POST['lock']) ?: $this->_checkLocked($_POST['lock'], $this->topic_info);
// Sticky can be null (no change), 1 sticky, 0 not sticky
$sticky = null;
if (isset($_POST['sticky']) && allowedTo('make_sticky'))
{
$sticky = (int) $_POST['sticky'];
}
$topicOptions = array(
'id' => empty($this->topic_id) ? 0 : $this->topic_id,
'board' => $board,
'lock_mode' => $lock,
'sticky_mode' => $sticky,
'mark_as_read' => $mark_as_read,
'is_approved' => !$modSettings['postmod_active'] || empty($this->topic_id) || !empty($board_info['cur_topic_approved']),
);
return $topicOptions;
}
public function savePost($msgOptions, $topicOptions, $posterOptions)
{
$this->_events->trigger('pre_save_post', array('msgOptions' => &$msgOptions, 'topicOptions' => &$topicOptions, 'posterOptions' => &$posterOptions));
// This is an already existing message. Edit it.
if (!empty($this->msg_id))
{
modifyPost($msgOptions, $topicOptions, $posterOptions);
}
// This is a new topic or an already existing one. Save it.
else
{
createPost($msgOptions, $topicOptions, $posterOptions);
if (isset($topicOptions['id']))
{
$this->topic_id = $topicOptions['id'];
}
}
$this->_events->trigger('after_save_post', array('msgOptions' => $msgOptions, 'topicOptions' => $topicOptions, 'becomes_approved' => $this->becomes_approved, 'posterOptions' => $posterOptions));
}
protected function setBecomesApproved()
{
global $modSettings, $user_info;
// If no postmod, it is always approved.
if (!$modSettings['postmod_active'])
{
return true;
}
if ($this->isModifyingMessage())
{
$this->becomes_approved = (allowedTo('approve_posts') && !$this->msgInfo['approved'] ? (!empty($_REQUEST['approve']) ? true : false) : $$this->msgInfo['approved']);
return $this->becomes_approved;
}
// In case we want to override
if (allowedTo('approve_posts'))
{
$this->becomes_approved = !isset($_REQUEST['approve']) || !empty($_REQUEST['approve']) ? true : false;
return $this>becomes_approved;
}
if($this->isNewTopic())
{
if (!allowedTo('post_new') && allowedTo('post_unapproved_topics'))
{
$this->becomes_approved = false;
}
else
{
isAllowedTo('post_new');
}
return $this->becomes_approved;
}
// Lastly, a new reply
if ($this->topic_info['id_member_started'] != $user_info['id'])
{
if (allowedTo('post_unapproved_replies_any') && !allowedTo('post_reply_any'))
{
$this->becomes_approved = false;
}
else
{
isAllowedTo('post_reply_any');
}
}
elseif (!allowedTo('post_reply_any'))
{
if (allowedTo('post_unapproved_replies_own') && !allowedTo('post_reply_own'))
{
$this->becomes_approved = false;
}
// Guests do not have post_unapproved_replies_own permission, so it's always post_unapproved_replies_any
elseif ($user_info['is_guest'] && allowedTo('post_unapproved_replies_any'))
{
$this->becomes_approved = false;
}
else
{
isAllowedTo('post_reply_own');
}
}
return $this->becomes_approved;
}
/**
*
* @param string $name
* @param string $email
* @return array
*/
protected function getPosterOptions($name, $email)
{
global $user_info, $board_info;
$posterOptions = array(
'id' => $user_info['id'],
'name' => $name,
'email' => $email,
'update_post_count' => !$user_info['is_guest'] && empty($this->msg_id) && $board_info['posts_count'],
);
return $posterOptions;
}
public function removeNestedQuotes($string)
{
global $modSettings;
if (!empty($modSettings['removeNestedQuotes']))
{
$string = preg_replace(array('~\n?\[quote.*?\].+?\[/quote\]\n?~is', '~^\n~', '~\[/quote\]~'), '', $string);
}
return $string;
}
public function isOldPost()
{
global $modSettings;
if (!empty($this->topic_id) && !empty($modSettings['oldTopicDays']) && $this->_topic_attributes['last_post_time'] + $modSettings['oldTopicDays'] * 86400 < time() && empty($this->_topic_attributes['is_sticky']) && !isset($_REQUEST['subject']))
{
$this->_post_errors->addError(array('old_topic', array($modSettings['oldTopicDays'])), 0);
}
}
public function hasNewRepliesSincePosting()
{
global $options;
return empty($options['no_new_reply_warning']) && isset($_REQUEST['last_msg']) && $this->topic_info['id_last_msg'] > $_REQUEST['last_msg'];
}
protected function newRepliesContext()
{
global $context, $txt, $modSettings;
addInlineJavascript('
$(document).ready(function () {
$("html,body").scrollTop($(\'.category_header:visible:first\').offset().top);
});'
);
$context['new_replies'] = countMessagesSince($this->topic_id, (int) $_REQUEST['last_msg'], false, $modSettings['postmod_active'] && !allowedTo('approve_posts'));
if (!empty($context['new_replies']))
{
if ($context['new_replies'] == 1)
{
$txt['error_new_replies'] = !empty($_REQUEST['last_msg']) ? $txt['error_new_reply_reading'] : $txt['error_new_reply'];
}
else
{
$txt['error_new_replies'] = sprintf(isset($_REQUEST['last_msg']) ? $txt['error_new_replies_reading'] : $txt['error_new_replies'], $context['new_replies']);
}
$this->_post_errors->addError('new_replies', 0);
$modSettings['topicSummaryPosts'] = $context['new_replies'] > $modSettings['topicSummaryPosts'] ? max($modSettings['topicSummaryPosts'], 5) : $modSettings['topicSummaryPosts'];
}
}
public function getPreviousPosts()
{
global $context, $modSettings, $user_info;
if (isset($_REQUEST['xml']))
{
$limit = empty($context['new_replies']) ? 0 : (int) $context['new_replies'];
}
else
{
$limit = $modSettings['topicSummaryPosts'];
}
$before = $this->isModifyingMessage() ? array('before' => (int) $this->msg_id) : array();
$only_approved = $modSettings['postmod_active'] && !allowedTo('approve_posts');
$counter = 0;
$context['previous_posts'] = empty($limit) ? array() : selectMessages($this->topic_id, 0, $limit, $before, $only_approved);
foreach ($context['previous_posts'] as &$post)
{
$post['is_new'] = !empty($context['new_replies']);
$post['counter'] = $counter++;
$post['is_ignored'] = !empty($modSettings['enable_buddylist']) && in_array($post['id_poster'], $user_info['ignoreusers']);
if (!empty($context['new_replies']))
{
$context['new_replies']--;
}
}
}
protected function iconContext()
{
global $board, $context;
// Message icons - customized or not, retrieve them...
$context['icons'] = getMessageIcons($board);
$context['icon_url'] = '';
if (!empty($context['icons']))
{
$context['icons'][count($context['icons']) - 1]['is_last'] = true;
$context['icons'][0]['selected'] = true;
// $context['icon'] is set when editing a message
if (!isset($context['icon']))
{
$context['icon'] = $context['icons'][0]['value'];
}
$found = false;
foreach ($context['icons'] as $icon)
{
if ($icon['value'] === $context['icon'])
{
$found = true;
$context['icon_url'] = $icon['url'];
break;
}
}
// Failsafe
if (!$found)
{
$context['icon'] = $context['icons'][0]['value'];
$context['icon_url'] = $context['icons'][0]['url'];
}
}
}
public function previewContext($subject, $body)
{
global $context, $modSettings, $txt;
// Set up the preview message and subject
$context['preview_message'] = $body;
preparsecode($body, true);
// Do all bulletin board code thing on the message
preparsecode($context['preview_message']);
$context['preview_message'] = parse_bbc($context['preview_message'], isset($_REQUEST['ns']) ? 0 : 1);
censorText($context['preview_message']);
// Don't forget the subject
$context['preview_subject'] = $subject;
censorText($context['preview_subject']);
// Any errors we should tell them about?
if ($subject === '')
{
$this->_post_errors->addError('no_subject');
$context['preview_subject'] = '<em>' . $txt['no_subject'] . '</em>';
}
if ($context['preview_message'] === '')
{
$this->_post_errors->addError('no_message');
}
elseif (!empty($modSettings['max_messageLength']) && Util::strlen($body) > $modSettings['max_messageLength'])
{
$this->_post_errors->addError(array('long_message', array($modSettings['max_messageLength'])));
}
// Protect any CDATA blocks.
if (isset($_REQUEST['xml']))
{
$context['preview_message'] = strtr($context['preview_message'], array(']]>' => ']]]]><![CDATA[>'));
}
}
protected function getIcon()
{
return isset($_REQUEST['icon']) ? preg_replace('~[\./\\\\*\':"<>]~', '', $_REQUEST['icon']) : 'xx';
}
protected function getEditorOptions()
{
global $context;
return array(
'id' => 'message',
'value' => $context['message'],
'labels' => array(
'post_button' => $context['submit_label'],
),
// add height and width for the editor
'height' => '275px',
'width' => '100%',
// We do XML preview here.
'preview_type' => 2
);
}
protected function postPageContext($subject, $body)
{
global $context, $scripturl, $txt, $modSettings;
// This is done for the editor
$context['subject'] = addcslashes($subject, '"');
$context['message'] = str_replace(array('"', '<', '>', '&nbsp;'), array('&quot;', '&lt;', '&gt;', ' '), $body);
// Get a response prefix (like 'Re:') in the default forum language.
$context['response_prefix'] = response_prefix();
// @todo These won't work if you're posting an event!
// Premissions
$context['can_notify'] = allowedTo('mark_any_notify');
$context['can_move'] = allowedTo('move_any');
$context['can_sticky'] = allowedTo('make_sticky');
$context['can_announce'] = allowedTo('announce_topic') && $context['becomes_approved'];
$context['can_quote'] = empty($modSettings['disabledBBC']) || !in_array('quote', explode(',', $modSettings['disabledBBC']));
$context['move'] = !empty($_REQUEST['move']);
$context['announce'] = !empty($_REQUEST['announce']);
$context['sticky'] = isset($_REQUEST['sticky']) ? !empty($_REQUEST['sticky']) : $this->_topic_attributes['is_sticky'];
$context['notify'] = !empty($context['notify']);
// You can only announce topics that will get approved...
$context['locked'] = !empty($this->_topic_attributes['locked']) || !empty($_REQUEST['lock']);
// Generally don't show the approval box... (Assume we want things approved)
$context['show_approval'] = allowedTo('approve_posts') && $context['becomes_approved'] ? 2 : (allowedTo('approve_posts') ? 1 : 0);
// Build the link tree.
if (empty($this->topic_id))
{
$context['linktree'][] = array(
'name' => '<em>' . $txt['start_new_topic'] . '</em>'
);
}
else
{
$context['linktree'][] = array(
'url' => $scripturl . '?topic=' . $this->topic_id . '.' . $_REQUEST['start'],
'name' => $context['subject'],
'extra_before' => '<span><strong class="nav">' . $context['page_title'] . ' ( </strong></span>',
'extra_after' => '<span><strong class="nav"> )</strong></span>'
);
}
// What are you doing? Posting, modifying, previewing, new post, or reply...
if (empty($context['page_title']))
{
if ($this->isModifyingMessage())
{
$context['page_title'] = $txt['modify_msg'];
}
elseif (isset($_REQUEST['subject'], $context['preview_subject']))
{
$context['page_title'] = $txt['post_reply'];
}
elseif ($this->isNewTopic())
{
$context['page_title'] = $txt['start_new_topic'];
}
else
{
$context['page_title'] = $txt['post_reply'];
}
}
$context['back_to_topic'] = isset($_REQUEST['goback']) || (!empty($this->msg_id) && !isset($_REQUEST['subject']));
$context['is_new_topic'] = $this->isNewTopic();
$context['is_new_post'] = $this->isNewReply();
$context['is_first_post'] = $context['is_new_topic'] || (!empty($this->msg_id) && $this->msg_id == $this->_topic_attributes['id_first_msg']);
$context['current_action'] = 'post';
// Needed for the editor and message icons.
require_once(SUBSDIR . '/Editor.subs.php');
// Now create the editor.
$editorOptions = $this->getEditorOptions();
$this->iconContext();
$context['show_additional_options'] = !empty($_REQUEST['additional_options']);
$this->_events->trigger('finalize_post_form', array('editorOptions' => &$editorOptions));
create_control_richedit($editorOptions);
}
public function setupPreview()
{
global $context, $user_info, $txt;
$this->_events->trigger('prepare_modifying', array('post_errors' => $this->_post_errors, 'really_previewing' => &$this->previewing));
// In order to keep the approval status flowing through, we have to pass it through the form...
$context['becomes_approved'] = empty($_REQUEST['not_approved']);
$context['show_approval'] = isset($_REQUEST['approve']) ? ($_REQUEST['approve'] ? 2 : 1) : 0;
$context['can_announce'] &= $context['becomes_approved'];
// Are you... a guest?
if ($user_info['is_guest'])
{
$context['name'] = $this->getGuestName(false);
$context['email'] = $this->getGuestEmail(false);
$user_info['name'] = $context['name'];
}
// Only show the preview stuff if they hit Preview.
if ($this->previewing === true)
{
$this->previewContext($form_subject, $form_message);
}
// Set up the checkboxes.
$context['notify'] = !empty($_REQUEST['notify']);
$context['use_smileys'] = !isset($_REQUEST['ns']);
$context['icon'] = $this->getIcon();
// Set the destination action for submission.
$context['destination'] .= !empty($this->msg_id) ? ';msg=' . $this->msg_id . ';' . $context['session_var'] . '=' . $context['session_id'] : '';
$context['submit_label'] = !empty($this->msg_id) ? $txt['save'] : $txt['post'];
// Previewing an edit?
if ($this->isModifyingMessage())
{
// Get the existing message.
$message = messageDetails((int) $this->msg_id, $this->topic_id);
// The message they were trying to edit was most likely deleted.
// @todo Change this error message?
if ($message === false)
{
Errors::instance()->fatal_lang_error('no_board', false);
}
$errors = checkMessagePermissions($message['message']);
if (!empty($errors))
{
foreach ($errors as $error)
{
$this->_post_errors->addError($error);
}
}
prepareMessageContext($message);
}
// No check is needed, since nothing is really posted.
checkSubmitOnce('free');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment