Вход Регистрация
Файл: library/XenForo/Model/Post.php
Строк: 2251
<?php

/**
 * Model for posts.
 *
 * @package XenForo_Post
 */
class XenForo_Model_Post extends XenForo_Model
{
    const 
FETCH_USER = 0x01;
    const 
FETCH_USER_PROFILE = 0x02;
    const 
FETCH_USER_OPTIONS = 0x04;
    const 
FETCH_THREAD = 0x08;
    const 
FETCH_FORUM = 0x10;
    const 
FETCH_DELETION_LOG = 0x20;
    const 
FETCH_BBCODE_CACHE = 0x40;
    const 
FETCH_NODE_PERMS = 0x80;

    
/**
     * Gets the named post.
     *
     * @param integer $postId
     *
     * @return array|false
     */
    
public function getPostById($postId, array $fetchOptions = array())
    {
        
$joinOptions = $this->preparePostJoinOptions($fetchOptions);

        return 
$this->_getDb()->fetchRow('
            SELECT post.*
                ' 
. $joinOptions['selectFields'] . '
            FROM xf_post AS post
            ' 
. $joinOptions['joinTables'] . '
            WHERE post.post_id = ?
        '
, $postId);
    }

    
/**
     * Gets the specified posts.
     *
     * @param array $postIds
     * @param array $fetchOptions Collection of options that relate to fetching
     *
     * @return array Format: [post id] => info
     */
    
public function getPostsByIds(array $postIds, array $fetchOptions = array())
    {
        if (!
$postIds)
        {
            return array();
        }

        
$joinOptions = $this->preparePostJoinOptions($fetchOptions);

        return 
$this->fetchAllKeyed('
            SELECT
                post.*
                ' 
. $joinOptions['selectFields'] . '
            FROM xf_post AS post' 
. $joinOptions['joinTables'] . '
            WHERE post.post_id IN (' 
. $this->_getDb()->quote($postIds) . ')
        '
, 'post_id');
    }

    
/**
     * Checks the 'join' key of the incoming array for the presence of the FETCH_x bitfields in this class
     * and returns SQL snippets to join the specified tables if required
     *
     * @param array $fetchOptions containing a 'join' integer key build from this class's FETCH_x bitfields
     *
     * @return array Containing 'selectFields' and 'joinTables' keys. Example: selectFields = ', user.*, foo.title'; joinTables = ' INNER JOIN foo ON (foo.id = other.id) '
     */
    
public function preparePostJoinOptions(array $fetchOptions)
    {
        
$selectFields = '';
        
$joinTables = '';

        
$db = $this->_getDb();

        if (!empty(
$fetchOptions['join']))
        {
            if (
$fetchOptions['join'] & self::FETCH_THREAD || $fetchOptions['join'] & self::FETCH_FORUM || $fetchOptions['join'] & self::FETCH_NODE_PERMS)
            {
                
$selectFields .= ',
                    thread.*, thread.user_id AS thread_user_id, thread.username AS thread_username,
                    thread.post_date AS thread_post_date,
                    post.user_id, post.username, post.post_date'
; // overwrite thread.post_date with post.post_date
                
$joinTables .= '
                    INNER JOIN xf_thread AS thread ON
                        (thread.thread_id = post.thread_id)'
;
            }

            if (
$fetchOptions['join'] & self::FETCH_FORUM)
            {
                
$selectFields .= ',
                    node.title AS node_title, node.node_name'
;
                
$joinTables .= '
                    INNER JOIN xf_node AS node ON
                        (node.node_id = thread.node_id)'
;
            }


            if (
XenForo_Application::getOptions()->cacheBbCodeTree && $fetchOptions['join'] & self::FETCH_BBCODE_CACHE)
            {
                
$selectFields .= ',
                    bb_code_parse_cache.parse_tree AS message_parsed, bb_code_parse_cache.cache_version AS message_cache_version'
;
                
$joinTables .= '
                    LEFT JOIN xf_bb_code_parse_cache AS bb_code_parse_cache ON
                        (bb_code_parse_cache.content_type = '
post' AND bb_code_parse_cache.content_id = post.post_id)';
            }

            if (
$fetchOptions['join'] & self::FETCH_USER || $fetchOptions['join'] & self::FETCH_NODE_PERMS)
            {
                
$selectFields .= ',
                    user.*, IF(user.username IS NULL, post.username, user.username) AS username'
;
                
$joinTables .= '
                    LEFT JOIN xf_user AS user ON
                        (user.user_id = post.user_id)'
;
            }

            if (
$fetchOptions['join'] & self::FETCH_USER_PROFILE)
            {
                
$selectFields .= ',
                    user_profile.*'
;
                
$joinTables .= '
                    LEFT JOIN xf_user_profile AS user_profile ON
                        (user_profile.user_id = post.user_id)'
;
            }

            if (
XenForo_Application::getOptions()->cacheBbCodeTree && $fetchOptions['join'] & self::FETCH_USER_PROFILE && $fetchOptions['join'] & self::FETCH_BBCODE_CACHE)
            {
                
$selectFields .= ',
                    signature_parse_cache.parse_tree AS signature_parsed, bb_code_parse_cache.cache_version AS signature_cache_version'
;
                
$joinTables .= '
                    LEFT JOIN xf_bb_code_parse_cache AS signature_parse_cache ON
                        (signature_parse_cache.content_type = '
signature' AND signature_parse_cache.content_id = post.user_id)';
            }

            if (
$fetchOptions['join'] & self::FETCH_USER_OPTIONS)
            {
                
$selectFields .= ',
                    user_option.*'
;
                
$joinTables .= '
                    LEFT JOIN xf_user_option AS user_option ON
                        (user_option.user_id = post.user_id)'
;
            }

            if (
$fetchOptions['join'] & self::FETCH_NODE_PERMS)
            {
                
$selectFields .= ',
                    permission.cache_value AS node_permission_cache'
;
                
$joinTables .= '
                    LEFT JOIN xf_permission_cache_content AS permission
                        ON (permission.permission_combination_id = user.permission_combination_id
                            AND permission.content_type = '
node'
                            AND permission.content_id = thread.node_id)'
;
            }

            if (
$fetchOptions['join'] & self::FETCH_DELETION_LOG)
            {
                
$selectFields .= ',
                    deletion_log.delete_date, deletion_log.delete_reason,
                    deletion_log.delete_user_id, deletion_log.delete_username'
;
                
$joinTables .= '
                    LEFT JOIN xf_deletion_log AS deletion_log ON
                        (deletion_log.content_type = '
post' AND deletion_log.content_id = post.post_id)';
            }
        }

        if (!empty(
$fetchOptions['permissionCombinationId']))
        {
            
$selectFields .= ',
                permission.cache_value AS node_permission_cache'
;
            
$joinTables .= '
                LEFT JOIN xf_permission_cache_content AS permission
                    ON (permission.permission_combination_id = ' 
. $db->quote($fetchOptions['permissionCombinationId']) . '
                        AND permission.content_type = '
node'
                        AND permission.content_id = thread.node_id)'
;
        }

        if (isset(
$fetchOptions['likeUserId']))
        {
            if (empty(
$fetchOptions['likeUserId']))
            {
                
$selectFields .= ',
                    0 AS like_date'
;
            }
            else
            {
                
$selectFields .= ',
                    liked_content.like_date'
;
                
$joinTables .= '
                    LEFT JOIN xf_liked_content AS liked_content
                        ON (liked_content.content_type = '
post'
                            AND liked_content.content_id = post.post_id
                            AND liked_content.like_user_id = ' 
.$db->quote($fetchOptions['likeUserId']) . ')';
            }
        }

        return array(
            
'selectFields' => $selectFields,
            
'joinTables'   => $joinTables
        
);
    }

    
/**
     * Returns all posts for a specified thread. Fetch options may limit
     * posts returned.
     *
     * @param integer $threadId
     * @param array $fetchOptions Collection of options that relate to fetching
     *
     * @return array
     */
    
public function getPostsInThread($threadId, array $fetchOptions = array())
    {
        
$limitOptions = $this->prepareLimitFetchOptions($fetchOptions);
        
$stateLimit = $this->prepareStateLimitFromConditions($fetchOptions, 'post');
        
$joinOptions = $this->preparePostJoinOptions($fetchOptions);

        return 
$this->fetchAllKeyed('
            SELECT post.*
                ' 
. $joinOptions['selectFields'] . '
            FROM xf_post AS post
            ' 
. $joinOptions['joinTables'] . '
            WHERE post.thread_id = ?
                ' 
. $this->addPositionLimit('post', $limitOptions['limit'], $limitOptions['offset']) . '
                AND (' 
. $stateLimit . ')
            ORDER BY post.position ASC, post.post_date ASC
        '
, 'post_id', $threadId);
    }

    
/**
     * Gets simple information about all posts in a thread. This does not
     * include the actual contents of a message unless specifically requested.
     *
     * @param integer $threadId
     * @param boolean $includeMessage If true, includes message contents
     *
     * @return array Format: [post id] => info
     */
    
public function getPostsInThreadSimple($threadId, $includeMessage = false)
    {
        return 
$this->fetchAllKeyed('
            SELECT post_id, thread_id, user_id, message_state, likes, post_date
                ' 
. ($includeMessage ? ', message' : '') . '
            FROM xf_post
            WHERE thread_id = ?
            ORDER BY position ASC, post_date ASC
        '
, 'post_id', $threadId);
    }

    public function 
getPostIdsInThread($threadId, $ordered = true)
    {
        return 
$this->_getDb()->fetchCol('
            SELECT post_id
            FROM xf_post
            WHERE thread_id = ?
            ' 
. ($ordered ? 'ORDER BY position ASC, post_date ASC' : '') . '
        '
, $threadId);
    }

    
/**
     * Fetches basic information about all posts made by $userId,
     * except those in threads started by $userId
     *
     * @param integer $userId
     *
     * @return array
     */
    
public function getPostsByUserInOthersThreads($userId)
    {
        return 
$this->fetchAllKeyed('
            SELECT post.*
            FROM xf_post AS post
            INNER JOIN xf_thread AS thread ON
                (thread.thread_id = post.thread_id)
            WHERE post.user_id = ?
                AND thread.user_id <> ?
        '
, 'post_id', array($userId, $userId));
    }

    
/**
     * Gets the latest post in the specified thread.
     *
     * @param integer $threadId
     * @param array $fetchOptions Collection of options that relate to fetching
     *
     * @return array|false
     */
    
public function getLastPostInThread($threadId, array $fetchOptions = array())
    {
        
$stateLimit = $this->prepareStateLimitFromConditions($fetchOptions, 'post');
        
$joinOptions = $this->preparePostJoinOptions($fetchOptions);

        
$db = $this->_getDb();
        return 
$db->fetchRow($db->limit('
            SELECT post.*
                ' 
. $joinOptions['selectFields'] . '
            FROM xf_post AS post
            ' 
. $joinOptions['joinTables'] . '
            WHERE post.thread_id = ?
                AND (' 
. $stateLimit . ')
            ORDER BY post.post_date DESC
        '
, 1), $threadId);
    }

    
/**
     * Gets the next post in a thread, post after the specified date. This is useful
     * for finding the first unread post in a thread, for example.
     *
     * @param integer $threadId
     * @param integer $postDate Finds first post posted after this
     * @param array $fetchOptions Collection of options that relate to fetching
     *
     * @return array|false
     */
    
public function getNextPostInThread($threadId, $postDate, array $fetchOptions = array(), array $ignoredUserIds = array())
    {
        
$stateLimit = $this->prepareStateLimitFromConditions($fetchOptions);

        
$db = $this->_getDb();

        return 
$db->fetchRow($db->limit('
            SELECT *
            FROM xf_post
            WHERE thread_id = ?
                AND post_date > ?
                AND (' 
. $stateLimit . ')
                ' 
. ($ignoredUserIds ? 'AND user_id NOT IN(' . $db->quote($ignoredUserIds) . ')' : '') . '
            ORDER BY post_date
        '
, 1), array($threadId, $postDate));
    }

    
/**
     * Returns the ID of the next post in the given thread after the specified position
     *
     * @param integer $threadId
     * @param integer $position
     * @param array $fetchOptions
     *
     * @return array
     */
    
public function getNextPostInThreadByPosition($threadId, $position, array $fetchOptions = array())
    {
        
$stateLimit = $this->prepareStateLimitFromConditions($fetchOptions);

        return 
$this->_getDb()->fetchRow($this->limitQueryResults('
            SELECT *
            FROM xf_post
            WHERE thread_id = ?
                AND position = ?
                AND (' 
. $stateLimit . ')
            ORDER BY post_date
        '
, 1), array($threadId, $position + 1));
    }

    
/**
     * Returns the newest posts for a specified thread, after the specified date.
     * Posts are returned newest first.
     *
     * @param integer $threadId
     * @param integer $postDate
     * @param array $fetchOptions
     *
     * @return array
     */
    
public function getNewestPostsInThreadAfterDate($threadId, $postDate, array $fetchOptions = array())
    {
        
$stateLimit = $this->prepareStateLimitFromConditions($fetchOptions, 'post');
        
$joinOptions = $this->preparePostJoinOptions($fetchOptions);
        
$limitOptions = $this->prepareLimitFetchOptions($fetchOptions);

        return 
$this->fetchAllKeyed($this->limitQueryResults(
            
'
                SELECT post.*
                    ' 
. $joinOptions['selectFields'] . '
                FROM xf_post AS post
                ' 
. $joinOptions['joinTables'] . '
                WHERE post.thread_id = ?
                    AND post.post_date > ?
                    AND (' 
. $stateLimit . ')
                ORDER BY post.post_date DESC
            '
, $limitOptions['limit'], $limitOptions['offset']
        ), 
'post_id', array($threadId, $postDate));
    }

    
/**
     * Counts the number of visible posts in the specified thread. This is the reply
     * count + 1.
     *
     * @param integer $threadId
     *
     * @return integer
     */
    
public function countVisiblePostsInThread($threadId)
    {
        return 
$this->_getDb()->fetchOne('
            SELECT COUNT(*)
            FROM xf_post
            WHERE thread_id = ?
                AND message_state = '
visible'
        '
, $threadId);
    }

    
/**
     * Gets post IDs in the specified range. The IDs returned will be those immediately
     * after the "start" value (not including the start), up to the specified limit.
     *
     * @param integer $start IDs greater than this will be returned
     * @param integer $limit Number of posts to return
     *
     * @return array List of IDs
     */
    
public function getPostIdsInRange($start, $limit)
    {
        
$db = $this->_getDb();

        return 
$db->fetchCol($db->limit('
            SELECT post_id
            FROM xf_post
            WHERE post_id > ?
            ORDER BY post_id
        '
, $limit), $start);
    }

    
/**
     * Gets the attachments that belong to the given posts, and merges them in with
     * their parent post (in the attachments key). The attachments key will not be
     * set if no attachments are found for the post.
     *
     * @param array $posts
     *
     * @return array Posts, with attachments added where necessary
     */
    
public function getAndMergeAttachmentsIntoPosts(array $posts)
    {
        
$postIds = array();

        foreach (
$posts AS $postId => $post)
        {
            if (
$post['attach_count'])
            {
                
$postIds[] = $postId;
            }
        }

        if (
$postIds)
        {
            
$attachmentModel = $this->_getAttachmentModel();

            
$attachments = $attachmentModel->getAttachmentsByContentIds('post', $postIds);

            foreach (
$attachments AS $attachment)
            {
                
$posts[$attachment['content_id']]['attachments'][$attachment['attachment_id']] = $attachmentModel->prepareAttachment($attachment);
            }
        }

        return 
$posts;
    }

    
/**
     * Determines if the post can be viewed with the given permissions.
     * This does not check thread/forum viewing permissions fully.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canViewPost(array $post, array $thread, array $forum, &$errorPhraseKey = '',
        array 
$nodePermissions = null, array $viewingUser = null
    
)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        if (!
XenForo_Permission::hasContentPermission($nodePermissions, 'view'))
        {
            return 
false;
        }

        if (
$this->isModerated($post))
        {
            if (!
$this->_getThreadModel()->canViewModeratedPosts($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser))
            {
                if (!
$viewingUser['user_id'] || $viewingUser['user_id'] != $post['user_id'])
                {
                    return 
false;
                }
            }
        }
        else if (
$this->isDeleted($post))
        {
            if (!
$this->_getThreadModel()->canViewDeletedPosts($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser))
            {
                return 
false;
            }
        }

        return 
true;
    }

    
/**
     * Determines if the post can be viewed with the given permissions.
     * TThis will check that any parent container can be viewed as well.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canViewPostAndContainer(array $post, array $thread, array $forum, &$errorPhraseKey = '',
        array 
$nodePermissions = null, array $viewingUser = null
    
)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        if (!
$this->_getThreadModel()->canViewThreadAndContainer($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser))
        {
            return 
false;
        }

        return 
$this->canViewPost($post, $thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser);
    }

    
/**
     * Determines if an attachment on this post can be viewed.
     * This does not check post viewing permissions.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canViewAttachmentOnPost(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        return 
$this->_getThreadModel()->canViewAttachmentsInThread($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser);
    }

    
/**
     * Determines if the post can be edited with the given permissions.
     * This does not check post viewing permissions.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canEditPost(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        if (!
$viewingUser['user_id'])
        {
            return 
false;
        }

        if (!
$thread['discussion_open']
            && !
$this->_getThreadModel()->canLockUnlockThread($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser))
        {
            
$errorPhraseKey = 'you_may_not_perform_this_action_because_discussion_is_closed';
            return 
false;
        }

        if (
XenForo_Permission::hasContentPermission($nodePermissions, 'editAnyPost'))
        {
            return 
true;
        }

        if (
$post['user_id'] == $viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'editOwnPost'))
        {
            
$editLimit = XenForo_Permission::hasContentPermission($nodePermissions, 'editOwnPostTimeLimit');

            if (
$editLimit != -1 && (!$editLimit || $post['post_date'] < XenForo_Application::$time - 60 * $editLimit))
            {
                
$errorPhraseKey = array('message_edit_time_limit_expired', 'minutes' => $editLimit);
                return 
false;
            }

            if (empty(
$forum['allow_posting']))
            {
                
$errorPhraseKey = 'you_may_not_perform_this_action_because_forum_does_not_allow_posting';
                return 
false;
            }

            return 
true;
        }

        return 
false;
    }

    
/**
     * Determines if the post's edit history can be viewed with the given permissions.
     * This does not check post viewing permissions.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canViewPostHistory(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        if (!
$viewingUser['user_id'])
        {
            return 
false;
        }

        if (
XenForo_Permission::hasContentPermission($nodePermissions, 'editAnyPost'))
        {
            return 
true;
        }

        
// TODO: permission
        
return false;
    }

    
/**
     * Determines if the silent edit-related options can be set with the given permissions.
     * This does not check post editing/viewing permissions.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canControlSilentEdit(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        return (
            
$viewingUser['user_id']
            && 
XenForo_Permission::hasContentPermission($nodePermissions, 'editAnyPost')
        );
    }

    
/**
     * Determines if the post can be deleted with the given permissions.
     * This does not check post viewing permissions.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $deleteType The type of deletion requested (soft or hard)
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canDeletePost(array $post, array $thread, array $forum, $deleteType = 'soft', &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        if (!
$viewingUser['user_id'])
        {
            return 
false;
        }

        if (
$deleteType != 'soft' && !XenForo_Permission::hasContentPermission($nodePermissions, 'hardDeleteAnyPost'))
        {
            
// fail immediately on hard delete without permission
            
return false;
        }

        if (
$post['post_id'] == $thread['first_post_id'])
        {
            
// would delete thread, so use that permission
            
return $this->_getThreadModel()->canDeleteThread(
                
$thread, $forum, $deleteType, $errorPhraseKey, $nodePermissions, $viewingUser
            
);
        }
        else if (
XenForo_Permission::hasContentPermission($nodePermissions, 'deleteAnyPost'))
        {
            return 
true;
        }
        else if (
$post['user_id'] == $viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'deleteOwnPost'))
        {
            
$editLimit = XenForo_Permission::hasContentPermission($nodePermissions, 'editOwnPostTimeLimit');

            if (
$editLimit != -1 && (!$editLimit || $post['post_date'] < XenForo_Application::$time - 60 * $editLimit))
            {
                
$errorPhraseKey = array('message_edit_time_limit_expired', 'minutes' => $editLimit);
                return 
false;
            }

            if (empty(
$forum['allow_posting']))
            {
                
$errorPhraseKey = 'you_may_not_perform_this_action_because_forum_does_not_allow_posting';
                return 
false;
            }

            return 
true;
        }

        return 
false;
    }

    
/**
     * Determines if the post can be undeleted with the given permissions.
     * This does not check post viewing permissions.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canUndeletePost(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        return (
$viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'undelete'));
    }

    
/**
     * Determines if the post can be approved or unapproved with the given permissions.
     * This does not check post viewing permissions.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canApproveUnapprovePost(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        return (
$viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'approveUnapprove'));
    }

    
/**
     * Determines if the post can be liked with the given permissions.
     * This does not check post viewing permissions.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canLikePost(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        if (!
$viewingUser['user_id'])
        {
            return 
false;
        }

        if (
$post['message_state'] != 'visible')
        {
            return 
false;
        }

        if (
$post['user_id'] == $viewingUser['user_id'])
        {
            
$errorPhraseKey = 'liking_own_content_cheating';
            return 
false;
        }

        return 
XenForo_Permission::hasContentPermission($nodePermissions, 'like');
    }

    
/**
     * Determines if the post can be moved to a new thread with the given permissions.
     * This does not check post viewing permissions.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canMovePost(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
        return (
$viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'manageAnyThread'));
    }

    
/**
     * Determines if the post can be copied to a new thread with the given permissions.
     * This does not check post viewing permissions.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canCopyPost(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
        return (
$viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'manageAnyThread'));
    }

    
/**
     * Determines if the post can be merged with another with the given permissions.
     * This does not check post viewing permissions.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canMergePost(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
        return (
$viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'manageAnyThread'));
    }

    
/**
     * Determines if the post can be warning with the given permissions.
     * This does not check post viewing permissions.
     *
     * @param array $post Info about the post
     * @param array $thread Info about the thread this post is in
     * @param array $forum Info about the forum the thread is in
     * @param string $errorPhraseKey Returned phrase key for a specific error
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canWarnPost(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        if (
$post['warning_id'] || empty($post['user_id']))
        {
            return 
false;
        }

        if (!empty(
$post['is_admin']) || !empty($post['is_moderator']))
        {
            return 
false;
        }

        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        return (
$viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'warn'));
    }

    
/**
     * Determines if the specified user can view IP addresses
     *
     * @param array $post
     * @param array $thread
     * @param array $forum
     * @param string $errorPhraseKey
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canViewIps(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
    {
        return 
$this->_getUserModel()->canViewIps($errorPhraseKey, $viewingUser);
    }

    
/**
     * Checks that the viewing user may report the specified post
     *
     * @param array $post
     * @param array $thread
     * @param array $forum
     * @param string
     * @param boolean $errorPhraseKey
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return boolean
     */
    
public function canReportPost(array $post, array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null ,array $viewingUser = null)
    {
        return 
$this->_getUserModel()->canReportContent($errorPhraseKey, $viewingUser);
    }

    
/**
     * Adds the canInlineMod value to the provided post and returns the
     * specific list of inline mod actions that are allowed on this post.
     *
     * @param array $post Post info
     * @param array $thread Thread the post is in
     * @param array $forum Forum the thread/post is in
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return array List of allowed inline mod actions, format: [action] => true
     */
    
public function addInlineModOptionToPost(array &$post, array $thread, array $forum, array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        
$postModOptions = array();
        
$canInlineMod = ($viewingUser['user_id'] && (
            
XenForo_Permission::hasContentPermission($nodePermissions, 'deleteAnyPost')
            || 
XenForo_Permission::hasContentPermission($nodePermissions, 'undelete')
            || 
XenForo_Permission::hasContentPermission($nodePermissions, 'approveUnapprove')
            || 
XenForo_Permission::hasContentPermission($nodePermissions, 'manageAnyThread')
        ));

        if (
$canInlineMod)
        {
            if (
$this->canDeletePost($post, $thread, $forum, 'soft', $null, $nodePermissions, $viewingUser))
            {
                
$postModOptions['delete'] = true;
            }
            if (
$this->canUndeletePost($post, $thread, $forum, $null, $nodePermissions, $viewingUser))
            {
                
$postModOptions['undelete'] = true;
            }
            if (
$this->canApproveUnapprovePost($post, $thread, $forum, $null, $nodePermissions, $viewingUser))
            {
                
$postModOptions['approve'] = true;
                
$postModOptions['unapprove'] = true;
            }
            if (
$this->canMovePost($post, $thread, $forum, $null, $nodePermissions, $viewingUser))
            {
                
$postModOptions['move'] = true;
            }
            if (
$this->canCopyPost($post, $thread, $forum, $null, $nodePermissions, $viewingUser))
            {
                
$postModOptions['copy'] = true;
            }
            if (
$this->canMergePost($post, $thread, $forum, $null, $nodePermissions, $viewingUser))
            {
                
$postModOptions['merge'] = true;
            }
        }

        
$post['canInlineMod'] = (count($postModOptions) > 0);

        return 
$postModOptions;
    }

    
/**
     * Gets the message state for a newly inserted post by the viewing user.
     *
     * @param array $thread Thread info, may be empty for a new thread
     * @param array $forum
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return string Message state (visible, moderated, deleted)
     */
    
public function getPostInsertMessageState(array $thread, array $forum, array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($forum['node_id'], $viewingUser, $nodePermissions);

        if (
$viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'approveUnapprove'))
        {
            return 
'visible';
        }
        else if (
XenForo_Permission::hasPermission($viewingUser['permissions'], 'general', 'followModerationRules'))
        {
            return (empty(
$forum['moderate_messages']) ? 'visible' : 'moderated');
        }
        else
        {
            return 
'moderated';
        }
    }

    
/**
     * Prepares a post for display, generally within the context of a thread.
     *
     * @param array $post Post to prepare
     * @param array $thread Thread post is in
     * @param array $forum Forum thread/post is in
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return array Prepared version of post
     */
    
public function preparePost(array $post, array $thread, array $forum, array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        if (!isset(
$post['canInlineMod']))
        {
            
$this->addInlineModOptionToPost($post, $thread, $forum, $nodePermissions, $viewingUser);
        }

        
$post['canEdit'] = $this->canEditPost($post, $thread, $forum, $null, $nodePermissions, $viewingUser);
        
$post['canViewHistory'] = $this->canViewPostHistory($post, $thread, $forum, $null, $nodePermissions, $viewingUser);
        
$post['canDelete'] = $this->canDeletePost($post, $thread, $forum, 'soft', $null, $nodePermissions, $viewingUser);
        
$post['canLike'] = $this->canLikePost($post, $thread, $forum, $null, $nodePermissions, $viewingUser);
        
$post['canReport'] = $this->canReportPost($post, $thread, $forum, $null, $nodePermissions, $viewingUser);
        
$post['canWarn'] = $this->canWarnPost($post, $thread, $forum, $null, $nodePermissions, $viewingUser);
        
$post['isFirst'] = ($post['post_id'] == $thread['first_post_id']);
        
$post['isDeleted'] = $this->isDeleted($post);
        
$post['isModerated'] = $this->isModerated($post);

        if (isset(
$thread['thread_read_date']) || isset($forum['forum_read_date']))
        {
            
$readOptions = array(0);
            if (isset(
$thread['thread_read_date'])) { $readOptions[] = $thread['thread_read_date']; }
            if (isset(
$forum['forum_read_date'])) { $readOptions[] = $forum['forum_read_date']; }

            
$post['isNew'] = (max($readOptions) < $post['post_date']);
        }
        else
        {
            
$post['isNew'] = false;
        }

        if (
array_key_exists('user_group_id', $post))
        {
            
$userModel = $this->_getUserModel();

            
$post = $userModel->prepareUser($post);
            
$post['canCleanSpam'] = (
                !empty(
$post['user_group_id'])
                && 
XenForo_Permission::hasPermission($viewingUser['permissions'], 'general', 'cleanSpam')
                && 
$userModel->couldBeSpammer($post)
            );
        }

        if (!empty(
$post['delete_date']))
        {
            
$post['deleteInfo'] = array(
                
'user_id' => $post['delete_user_id'],
                
'username' => $post['delete_username'],
                
'date' => $post['delete_date'],
                
'reason' => $post['delete_reason'],
            );
        }

        if (
$post['likes'])
        {
            
$post['likeUsers'] = unserialize($post['like_users']);
        }

        return 
$post;
    }

    
/**
     * Gets permission-based options that apply to post fetching functions.
     *
     * @param array $thread Thread the posts will belong to
     * @param array $forum Forum the thread belongs to
     * @param array|null $nodePermissions
     * @param array|null $viewingUser
     *
     * @return array Keys: deleted/moderated (both booleans)
     */
    
public function getPermissionBasedPostFetchOptions(array $thread, array $forum, array $nodePermissions = null, array $viewingUser = null)
    {
        
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);

        if (
XenForo_Permission::hasContentPermission($nodePermissions, 'viewModerated'))
        {
            
$viewModerated = true;
        }
        else if (
$viewingUser['user_id'])
        {
            
$viewModerated = $viewingUser['user_id'];
        }
        else
        {
            
$viewModerated = false;
        }

        return array(
            
'deleted' => XenForo_Permission::hasContentPermission($nodePermissions, 'viewDeleted'),
            
'moderated' => $viewModerated
        
);
    }

    
/**
     * Helper to delete the specified post, via a soft or hard delete.
     *
     * @param integer $postId ID of the post to delete
     * @param string $deleteType Type of deletion (soft or hard)
     * @param array $options Deletion options.
     * @param array $forum The forum containing this post
     *
     * @return XenForo_DataWriter_DiscussionMessage_Post The DW used to delete the post
     */
    
public function deletePost($postId, $deleteType, array $options = array(), array $forum = null)
    {
        
//TODO: logging of this action

        
$options = array_merge(array(
            
'reason' => ''
        
), $options);

        
$dw = XenForo_DataWriter::create('XenForo_DataWriter_DiscussionMessage_Post');
        
$dw->setExistingData($postId);

        if (empty(
$forum))
        {
            
$forum = $this->getModelFromCache('XenForo_Model_Forum')->getForumByThreadId($dw->get('thread_id'));
        }

        
$dw->setExtraData(XenForo_DataWriter_DiscussionMessage_Post::DATA_FORUM, $forum);

        if (
$deleteType == 'hard')
        {
            
$dw->delete();
        }
        else
        {
            
$dw->setExtraData(XenForo_DataWriter_DiscussionMessage::DATA_DELETE_REASON, $options['reason']);
            
$dw->set('message_state', 'deleted');
            
$dw->save();
        }

        return 
$dw;
    }

    
/**
     * From a list of post IDs, gets info about the posts, their threads, and
     * the forums the threads are in.
     *
     * If a permission combination ID is passed, the forums will retrieve permission info.
     *
     * @param array $postIds List of post IDs
     * @param integer $permissionCombinationId Permission combination ID that will be retrieved with the forums.
     *
     * @return array Format: [0] => list of posts, [1] => list of threads, [2] => list of forums
     */
    
public function getPostsAndParentData(array $postIds, $permissionCombinationId = null)
    {
        if (
$permissionCombinationId === null)
        {
            
$visitor = XenForo_Visitor::getInstance();
            
$permissionCombinationId = $visitor['permission_combination_id'];
        }

        
$threads = array();
        
$forums = array();
        
$posts = $this->getPostsByIds($postIds);

        if (
$posts)
        {
            
$threadIds = array();
            foreach (
$posts AS $post)
            {
                
$threadIds[$post['post_id']] = $post['thread_id'];
            }

            list(
$threads, $forums) = $this->_getThreadModel()->getThreadsAndParentData($threadIds, $permissionCombinationId);

            foreach (
$posts AS $postId => $post)
            {
                if (!isset(
$threads[$post['thread_id']]))
                {
                    unset(
$posts[$postId]);
                }
            }
        }

        return array(
$posts, $threads, $forums);
    }

    
/**
     * Recalculates the position of all posts in the thread, and returns info relating
     * to the thread: firstPostId, firstPostDate, firstPostState, lastPostId, visibleCount.
     *
     * @param integer $threadId
     *
     * @return array
     */
    
public function recalculatePostPositionsInThread($threadId)
    {
        
$db = $this->_getDb();

        
$postResults = $this->_getDb()->query('
            SELECT post_id, user_id, username, post_date, message_state, position
            FROM xf_post
            WHERE thread_id = ?
            ORDER BY post_date, post_id
        '
, $threadId);

        
$position = 0;
        
$firstPost = array();
        
$lastPost = array();
        
$userPosts = array();
        
$updatePositions = array();

        while (
$post = $postResults->fetch())
        {
            if (
$post['position'] != $position)
            {
                
$updatePositions[$post['post_id']] = $position;
            }

            if (!
$firstPost)
            {
                
$firstPost = $post;
            }

            if (
$post['message_state'] == 'visible')
            {
                
$lastPost = $post;
                
$position++;

                if (
$post['user_id'])
                {
                    if (isset(
$userPosts[$post['user_id']]))
                    {
                        
$userPosts[$post['user_id']]++;
                    }
                    else
                    {
                        
$userPosts[$post['user_id']] = 1;
                    }
                }
            }
        }


        if (!
$firstPost)
        {
            return array(
                
'firstPostId' => 0,
                
'lastPostId' => 0,
                
'visibleCount' => 0,
                
'userPosts' => array()
            );
        }

        if (
$updatePositions)
        {
            
XenForo_Db::beginTransaction($db);

            foreach (
$updatePositions AS $postId => $updatePosition)
            {
                
$db->update('xf_post', array('position' => $updatePosition), 'post_id = ' . $db->quote($postId));
            }

            
XenForo_Db::commit($db);
        }

        if (!
$lastPost)
        {
            
$lastPost = $firstPost;
        }

        return array(
            
'firstPostId' => $firstPost['post_id'],
            
'firstPostDate' => $firstPost['post_date'],
            
'firstPostState' => $firstPost['message_state'],
            
'firstPost' => $firstPost,

            
'lastPostId' => $lastPost['post_id'],
            
'lastPost' => $lastPost,

            
'visibleCount' => $position,
            
'userPosts' => $userPosts
        
);
    }

    
/**
     * Moves the specified posts (in the given threads) to a new thread. The
     * new thread will be created in this function if necessary.
     *
     * @param array $posts
     * @param array $sourceThreads
     * @param array $newThread Information about the new thread or target thread
     * @param array $options Options to control behavior
     *
     * @return array|false New thread ID or false
     */
    
public function movePosts(array $posts, array $sourceThreads, array $newThread, array $options = array())
    {
        return 
$this->_moveOrCopyPosts('move', $posts, $sourceThreads, $newThread, $options);
    }

    
/**
     * Moves the specified posts (in the given threads) to a new thread. The
     * new thread will be created in this function if necessary.
     *
     * @param array $posts
     * @param array $sourceThreads
     * @param array $targetThread Information about the new thread or target thread
     * @param array $options Options to control behavior
     *
     * @return array|false New thread ID or false
     */
    
public function copyPosts(array $posts, array $sourceThreads, array $targetThread, array $options = array())
    {
        return 
$this->_moveOrCopyPosts('copy', $posts, $sourceThreads, $targetThread, $options);
    }

    
/**
     * Implements post move/copy logic. If the target thread doesn't have a thread ID,
     * the thread will be created automatically.
     *
     * @param string $action "move" or "copy"
     * @param array $posts
     * @param array $sourceThreads
     * @param array $targetThread
     * @param array $options
     *
     * @return array|bool Target thread info on success, false otherwise
     *
     * @throws XenForo_Exception
     */
    
protected function _moveOrCopyPosts($action, array $posts, array $sourceThreads, array $targetThread, array $options = array())
    {
        switch (
$action)
        {
            case 
'move':
            case 
'copy':
                break;

            default:
                throw new 
XenForo_Exception("Unknown post move/copy action $action");
        }

        if (!
$posts)
        {
            return 
false;
        }

        
$forum = $this->getModelFromCache('XenForo_Model_Forum')->getForumById($targetThread['node_id']);
        if (!
$forum)
        {
            return 
false;
        }

        
$firstPostId = 0;
        
$firstPostDate = PHP_INT_MAX;
        
$sourceThreadIds = array();

        
$options = array_merge(array(
            
'log' => true
        
), $options);

        foreach (
$posts AS $postId => $post)
        {
            
$sourceThreadIds[$post['thread_id']] = true;

            if (
$post['post_date'] < $firstPostDate
                
|| ($post['post_date'] == $firstPostDate && $post['post_id'] < $firstPostId)
            )
            {
                
$firstPostId = $postId;
                
$firstPostDate = $post['post_date'];
            }
        }

        if (!isset(
$posts[$firstPostId]))
        {
            return 
false;
        }

        
$firstPost = $posts[$firstPostId];

        
$db = $this->_getDb();
        
XenForo_Db::beginTransaction($db);

        
$targetExisting = !empty($targetThread['thread_id']);

        if (!
$targetExisting)
        {
            
$targetThread['post_date'] = $firstPost['post_date'];
            
$targetThread['user_id'] = $firstPost['user_id'];
            
$targetThread['username'] = $firstPost['username'];
            
$targetThread['discussion_state'] = $firstPost['message_state'];

            
$threadDw = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread');
            
$threadDw->setOption(XenForo_DataWriter_Discussion::OPTION_REQUIRE_INSERT_FIRST_MESSAGE, false);
            
$threadDw->bulkSet($targetThread);
            
$threadDw->save();
            
$targetThread = $threadDw->getMergedData();
        }

        if (
$action == 'copy')
        {
            
$newPostIds = array();
            foreach (
$posts AS $post)
            {
                
$newPost = $this->_copyPost($post, $targetThread, $forum);
                
$newPostIds[] = $newPost['post_id'];

                if (
$post['post_id'] == $firstPostId)
                {
                    
$firstPost = $newPost;
                    
$firstPostId = $newPost['post_id'];
                }
            }
        }
        else
        {
            
$postIds = array_keys($posts);
            
$quotedIds = $db->quote($postIds);

            
$db->update('xf_post',
                array(
'thread_id' => $targetThread['thread_id']),
                
'post_id IN (' . $quotedIds . ')'
            
);

            
// when moving posts, remove alerts for these posts because
            // they'll reflect the new thread instead which is just confusing
            
$db->query("
                DELETE FROM xf_user_alert
                WHERE content_type = 'post'
                    AND content_id IN (
$quotedIds)
                    AND action IN ('insert', 'insert_attachment')
            "
);

            
$newPostIds = $postIds;
        }

        if (
$firstPost['post_date'] <= $targetThread['post_date'])
        {
            
$firstPostDw = XenForo_DataWriter::create('XenForo_DataWriter_DiscussionMessage_Post', XenForo_DataWriter::ERROR_SILENT);
            
$firstPostDw->setExistingData($firstPostId);
            
$firstPostDw->set('message_state', 'visible');
            
$firstPostDw->setExtraData(XenForo_DataWriter_DiscussionMessage_Post::DATA_FORUM, $forum);
            
$firstPostDw->save();
        }

        
$threadDw = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread');
        
$threadDw->setExistingData($targetThread, true);
        
$threadDw->rebuildDiscussion();
        
$threadDw->save();

        
$sourceForums = $this->getModelFromCache('XenForo_Model_Forum')->getForumsByThreadIds($sourceThreadIds);
        
$newThreadUrl = XenForo_Link::buildPublicLink('threads', $targetThread);

        foreach (
$sourceThreads AS $sourceThread)
        {
            
$threadDw = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread');
            
$threadDw->setExistingData($sourceThread, true);
            if (isset(
$sourceForums[$sourceThread['node_id']]))
            {
                
$threadDw->setExtraData(XenForo_DataWriter_Discussion_Thread::DATA_FORUM, $sourceForums[$sourceThread['node_id']]);
            }

            
$threadValid = $threadDw->rebuildDiscussion();
            if (
$threadValid)
            {
                
$threadDw->save();

                if (
$options['log'])
                {
                    
XenForo_Model_Log::logModeratorAction(
                        
'thread', $sourceThread, 'post_' . $action . '_source', array('url' => $newThreadUrl, 'title' => $targetThread['title'])
                    );
                }
            }
            else
            {
                
// all posts removed -> delete
                
$threadDw->delete();
            }
        }

        if (
$options['log'])
        {
            
$modKey = 'post_' . $action . '_target' . ($targetExisting ? '_existing' : '');

            
XenForo_Model_Log::logModeratorAction(
                
'thread', $targetThread, $modKey, array('ids' => implode(', ', $newPostIds))
            );
        }

        if (
$newPostIds)
        {
            
XenForo_Application::defer('SearchIndexPartial', array('contentType' => 'post', 'contentIds' => $newPostIds));
        }

        
XenForo_Db::commit($db);

        return 
$targetThread;
    }

    
/**
     * Handles the actual copying of a post and all related information
     *
     * @param array $post
     * @param array $targetThread
     * @param array $forum
     *
     * @return array New post content
     */
    
protected function _copyPost(array $post, array $targetThread, array $forum)
    {
        
$db = $this->_getDb();

        
$oldPostId = $post['post_id'];

        
$newPost = $post;
        unset(
$newPost['post_id']);
        
$newPost['thread_id'] = $targetThread['thread_id'];
        
$newPost['likes'] = 0;
        
$newPost['like_users'] = 'a:0:{}';
        
$newPost['warning_id'] = 0;
        
$newPost['warning_message'] = '';
        
$newPost['last_edit_date'] = 0;
        
$newPost['last_edit_user_id'] = 0;
        
$newPost['edit_count'] = 0;

        
$db->insert('xf_post', $newPost);
        
$postId = $db->lastInsertId();
        
$newPost['post_id'] = $postId;

        if (
$newPost['attach_count'])
        {
            
$message = $newPost['message'];

            
$attachments = $db->fetchAll("
                SELECT *
                FROM xf_attachment
                WHERE content_type = 'post'
                    AND content_id = ?
            "
, $oldPostId);
            foreach (
$attachments AS $attachment)
            {
                
$oldAttachmentId = $attachment['attachment_id'];

                unset(
$attachment['attachment_id']);
                
$attachment['content_id'] = $postId;
                
$attachment['view_count'] = 0;

                
$db->insert('xf_attachment', $attachment);
                
$newAttachmentId = $db->lastInsertId();
                
$db->query("
                    UPDATE xf_attachment_data
                    SET attach_count = attach_count + 1
                    WHERE data_id = ?
                "
, $attachment['data_id']);

                
$message = preg_replace(
                    
'#([attach(=[^]]+)?])' . $oldAttachmentId . '([/attach])#i',
                    
'${1}' . $newAttachmentId . '${3}',
                    
$message
                
);
            }

            
$db->query("
                UPDATE xf_post
                SET message = ?
                WHERE post_id = ?
            "
, array($message, $newPost['post_id']));
        }

        if (
$newPost['message_state'] == 'deleted')
        {
            
$delete = $db->fetchRow("
                SELECT *
                FROM xf_deletion_log
                WHERE content_type = 'post'
                    AND content_id = ?
            "
, $oldPostId);
            if (
$delete)
            {
                
$delete['content_id'] = $postId;
                
$db->insert('xf_deletion_log', $delete);
            }
        }
        else if (
$newPost['message_state'] == 'moderated')
        {
            
$moderate = $db->fetchRow("
                SELECT *
                FROM xf_moderation_queue
                WHERE content_type = 'post'
                    AND content_id = ?
            "
, $oldPostId);
            if (
$moderate)
            {
                
$moderate['content_id'] = $postId;
                
$db->insert('xf_moderation_queue', $moderate);
            }
        }

        if (
$targetThread['discussion_state'] == 'visible'
            
&& $newPost['message_state'] == 'visible'
            
&& $newPost['user_id']
            && !empty(
$forum['count_messages'])
        )
        {
            
$db->query("
                UPDATE xf_user
                SET message_count = message_count + 1
                WHERE user_id = ?
            "
, $newPost['user_id']);
        }

        return 
$newPost;
    }

    
/**
     * Merges specified posts (from given threads) into a target post and updates the text.
     * The target post must be in the list of given posts.
     *
     * @param array $posts
     * @param array $threads
     * @param integer $targetPostId
     * @param string $newMessage
     * @param array $options
     *
     * @return boolean
     */
    
public function mergePosts(array $posts, array $threads, $targetPostId, $newMessage, $options = array())
    {
        if (!isset(
$posts[$targetPostId]))
        {
            return 
false;
        }

        
$options = array_merge(array(
            
'log' => true
        
), $options);

        
$targetPost = $posts[$targetPostId];
        unset(
$posts[$targetPostId]);

        if (!
$posts)
        {
            return 
false;
        }

        
$attachPosts = array();
        
$likePosts = array();
        foreach (
$posts AS $postId => $post)
        {
            if (
$post['attach_count'])
            {
                
$attachPosts[$postId] = $post['attach_count'];
            }
            if (
$post['likes'])
            {
                
$likePosts[$postId] = $post['likes'];
            }
        }

        
$db = $this->_getDb();
        
XenForo_Db::beginTransaction($db);

        
$forums = $this->getModelFromCache('XenForo_Model_Forum')->getForumsByThreadIds(array_keys($threads));

        
$targetPostDw = XenForo_DataWriter::create('XenForo_DataWriter_DiscussionMessage_Post');
        
$targetPostDw->setExistingData($targetPost, true);
        
$targetPostDw->setOption(XenForo_DataWriter_DiscussionMessage::OPTION_IS_AUTOMATED, true);
        
$targetPostDw->set('message', $newMessage);

        if (
array_key_exists($targetPostDw->get('thread_id'), $threads))
        {
            
$thread = $threads[$targetPostDw->get('thread_id')];

            if (
array_key_exists($thread['node_id'], $forums))
            {
                
$targetPostDw->setExtraData(XenForo_DataWriter_DiscussionMessage_Post::DATA_FORUM, $forums[$thread['node_id']]);
            }
        }

        if (
$likePosts)
        {
            
$res = $db->query("
                UPDATE IGNORE xf_liked_content SET
                    content_id = ?
                WHERE content_type = 'post' AND content_id IN (" 
. $db->quote(array_keys($likePosts)) . ')
            '
, $targetPost['post_id']);
            
$likesMoved = $res->rowCount();
            
// remaining likes will be deleted with the posts, should keep counts accurate

            
$latestLikeUsers = $this->getModelFromCache('XenForo_Model_Like')->getLatestContentLikeUsers(
                
'post', $targetPost['post_id']
            );
            
$targetPostDw->set('likes', $targetPostDw->get('likes') + $likesMoved);
            
$targetPostDw->set('like_users', $latestLikeUsers);
        }

        if (
$attachPosts)
        {
            
$db->update('xf_attachment',
                array(
'content_id' => $targetPost['post_id']),
                
"content_type = 'post' AND content_id IN (" . $db->quote(array_keys($attachPosts)) . ')'
            
);
        }
        
$targetPostDw->set('attach_count', $targetPostDw->get('attach_count') + array_sum($attachPosts));

        
$targetPostDw->save();

        foreach (
$posts AS $post)
        {
            
$sourcePostDw = XenForo_DataWriter::create('XenForo_DataWriter_DiscussionMessage_Post');
            
$sourcePostDw->setOption(XenForo_DataWriter_DiscussionMessage::OPTION_DELETE_DISCUSSION_FIRST_MESSAGE, false);
            
$sourcePostDw->setExistingData($post, true);
            
$sourcePostDw->set('attach_count', 0); // moved these away, no need to try to delete

            
if (array_key_exists($sourcePostDw->get('thread_id'), $threads))
            {
                
$thread = $threads[$sourcePostDw->get('thread_id')];

                if (
array_key_exists($thread['node_id'], $forums))
                {
                    
$sourcePostDw->setExtraData(XenForo_DataWriter_DiscussionMessage_Post::DATA_FORUM, $forums[$thread['node_id']]);
                }
            }

            
$sourcePostDw->delete();
        }

        if (
$options['log'])
        {
            
XenForo_Model_Log::logModeratorAction(
                
'post', $targetPost, 'merge_target', array('ids' => implode(', ', array_keys($posts)))
            );
        }

        
XenForo_Db::commit($db);

        return 
true;
    }

    
/**
     * Gets the quote text for the specified post.
     *
     * @param array $post
     * @param integer $maxQuoteDepth Max depth of quoted text (-1 for unlimited)
     *
     * @return string
     */
    
public function getQuoteTextForPost(array $post, $maxQuoteDepth = 0)
    {
        if (
$post['message_state'] != 'visible')
        {
            
// non-visible posts shouldn't be quoted
            
return '';
        }

        
/*
         * Note that if this syntax changes, changes must be applied to
         * XenForo_Model_Post::alertQuotedMembers()
         * and to
         * XenForo_BbCode_Formatter_Base::renderTagQuote()
         */
        
return '[QUOTE="' . $post['username']
            . 
', post: ' . $post['post_id']
            . (!empty(
$post['user_id']) ? ', member: ' . $post['user_id'] : '')
            . 
'"]'
            
. trim(XenForo_Helper_String::stripQuotes($post['message'], $maxQuoteDepth))
            . 
"[/QUOTE]n";
    }

    
/**
     * Sends an alert to members directly quoted in a post
     *
     * @param array $post
     * @param array $thread
     * @param array $forum
     */
    
public function alertQuotedMembers(array $post, array $thread, array $forum)
    {
        
$quotedUsers = array();

        if (
preg_match_all('/[quote=("|'|)([^,]+),s*post:s*(d+?).*\1]/siU', $post['message'], $quotes))
        {
            $quotedPosts = $this->getPostsByIds(array_unique($quotes[3]), array(
                '
join' => XenForo_Model_Post::FETCH_USER_OPTIONS
                    | XenForo_Model_Post::FETCH_USER_PROFILE
                    | XenForo_Model_Post::FETCH_THREAD
                    | XenForo_Model_Post::FETCH_FORUM
                    | XenForo_Model_Post::FETCH_NODE_PERMS
            ));
            $userModel = $this->_getUserModel();

            foreach ($quotedPosts AS $quotedPostId => $quotedPost)
            {
                if (!isset($quotedUsers[$quotedPost['
user_id']]) && $quotedPost['user_id'] && $quotedPost['user_id'] != $post['user_id'])
                {
                    $permissions = XenForo_Permission::unserializePermissions($quotedPost['
node_permission_cache']);

                    if (!$userModel->isUserIgnored($quotedPost, $post['
user_id'])
                        && XenForo_Model_Alert::userReceivesAlert($quotedPost, '
post', 'quote')
                        && $this->canViewPostAndContainer($quotedPost, $quotedPost, $quotedPost, $null, $permissions, $quotedPost)
                    )
                    {
                        $quotedUsers[$quotedPost['
user_id']] = true;

                        XenForo_Model_Alert::alert($quotedPost['
user_id'],
                            $post['
user_id'], $post['username'],
                            '
post', $post['post_id'],
                            '
quote'
                        );
                    }
                }
            }
        }

        return array_keys($quotedUsers);
    }

    public function alertTaggedMembers(array $post, array $thread, array $forum, array $tagged, array $alreadyAlerted)
    {
        $userIds = XenForo_Application::arrayColumn($tagged, '
user_id');
        $userIds = array_diff($userIds, $alreadyAlerted);
        $alertedUserIds = array();

        if ($userIds)
        {
            $userModel = $this->_getUserModel();
            $users = $userModel->getUsersByIds($userIds, array(
                '
join' => XenForo_Model_User::FETCH_USER_OPTION | XenForo_Model_User::FETCH_USER_PROFILE,
                '
nodeIdPermissions' => $thread['node_id']
            ));
            foreach ($users AS $user)
            {
                if (!isset($alertedUserIds[$user['
user_id']]) && $user['user_id'] != $post['user_id'])
                {
                    $permissions = XenForo_Permission::unserializePermissions($user['
node_permission_cache']);

                    if (!$userModel->isUserIgnored($user, $post['
user_id'])
                        && XenForo_Model_Alert::userReceivesAlert($user, '
post', 'tag')
                        && $this->canViewPostAndContainer($post, $thread, $forum, $null, $permissions, $user)
                    )
                    {
                        $alertedUserIds[$user['
user_id']] = true;

                        XenForo_Model_Alert::alert($user['
user_id'],
                            $post['
user_id'], $post['username'],
                            '
post', $post['post_id'],
                            '
tag'
                        );
                    }
                }
            }
        }

        return array_keys($alertedUserIds);
    }

    /**
     * Attempts to update any instances of an old username in like_users with a new username
     *
     * @param integer $oldUserId
     * @param integer $newUserId
     * @param string $oldUsername
     * @param string $newUsername
     */
    public function batchUpdateLikeUser($oldUserId, $newUserId, $oldUsername, $newUsername)
    {
        $db = $this->_getDb();

        // note that xf_liked_content should have already been updated with $newUserId

        $db->query('
            
UPDATE (
                
SELECT content_id FROM xf_liked_content
                WHERE content_type 
= 'post'
                
AND like_user_id = ?
            ) AS 
temp
            INNER JOIN xf_post 
AS post ON (post.post_id = temp.content_id)
            
SET like_users = REPLACE(like_users, ' .
                $db->quote('
i:' . $oldUserId . ';s:8:"username";s:' . strlen($oldUsername) . ':"' . $oldUsername . '";') . ', ' .
                $db->quote('
i:' . $newUserId . ';s:8:"username";s:' . strlen($newUsername) . ':"' . $newUsername . '";') . ')
        
', $newUserId);
    }

    /**
     * Checks whether a post is marked as deleted
     *
     * @param array $post
     *
     * @return boolean
     */
    public function isDeleted(array $post)
    {
        if (!isset($post['
message_state']))
        {
            throw new XenForo_Exception('
Message state not available in post.');
        }

        return ($post['
message_state'] == 'deleted');
    }

    /**
     * Checks whether a post is marked as moderated
     *
     * @param array $post
     *
     * @return boolean
     */
    public function isModerated(array $post)
    {
        if (!isset($post['
message_state']))
        {
            throw new XenForo_Exception('
Message state not available in post.');
        }

        return ($post['
message_state'] == 'moderated');
    }

    /**
     * @return XenForo_Model_Thread
     */
    protected function _getThreadModel()
    {
        return $this->getModelFromCache('
XenForo_Model_Thread');
    }

    /**
     * @return XenForo_Model_User
     */
    protected function _getUserModel()
    {
        return $this->getModelFromCache('
XenForo_Model_User');
    }

    /**
     * @return XenForo_Model_Attachment
     */
    protected function _getAttachmentModel()
    {
        return $this->getModelFromCache('
XenForo_Model_Attachment');
    }
}
Онлайн: 2
Реклама