Файл: library/XenForo/Model/Thread.php
Строк: 2711
<?php
/**
* Model for threads.
*
* @package XenForo_Thread
*/
class XenForo_Model_Thread extends XenForo_Model
{
/**
* Constants to allow joins to extra tables in certain queries
*
* @var integer Join user table
* @var integer Join node table
* @var integer Join post table
* @var integer Join user table to fetch avatar info of first poster
* @var integer Join forum table to fetch forum options
*/
const FETCH_USER = 0x01;
const FETCH_FORUM = 0x02;
const FETCH_FIRSTPOST = 0x04;
const FETCH_AVATAR = 0x08;
const FETCH_DELETION_LOG = 0x10;
const FETCH_FORUM_OPTIONS = 0x20;
/**
* Returns a thread record based
*
* @param integer $threadId
* @param array $fetchOptions Collection of options related to fetching
*
* @return array|false
*/
public function getThreadById($threadId, array $fetchOptions = array())
{
$joinOptions = $this->prepareThreadFetchOptions($fetchOptions);
return $this->_getDb()->fetchRow('
SELECT thread.*
' . $joinOptions['selectFields'] . '
FROM xf_thread AS thread
' . $joinOptions['joinTables'] . '
WHERE thread.thread_id = ?
', $threadId);
}
/**
* Gets the named threads.
*
* @param array $threadIds
* @param array $fetchOptions Collection of options related to fetching
*
* @return array Format: [thread id] => info
*/
public function getThreadsByIds(array $threadIds, array $fetchOptions = array())
{
if (!$threadIds)
{
return array();
}
$joinOptions = $this->prepareThreadFetchOptions($fetchOptions);
return $this->fetchAllKeyed('
SELECT thread.*
' . $joinOptions['selectFields'] . '
FROM xf_thread AS thread' . $joinOptions['joinTables'] . '
WHERE thread.thread_id IN (' . $this->_getDb()->quote($threadIds) . ')
', 'thread_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, joinTables, orderClause keys.
* Example: selectFields = ', user.*, foo.title'; joinTables = ' INNER JOIN foo ON (foo.id = other.id) '; orderClause = ORDER BY x.y
*/
public function prepareThreadFetchOptions(array $fetchOptions)
{
$selectFields = '';
$joinTables = '';
$orderBy = '';
if (!empty($fetchOptions['order']))
{
$orderBySecondary = '';
switch ($fetchOptions['order'])
{
case 'title':
case 'post_date':
case 'view_count':
$orderBy = 'thread.' . $fetchOptions['order'];
break;
case 'reply_count':
case 'first_post_likes':
$orderBy = 'thread.' . $fetchOptions['order'];
$orderBySecondary = ', thread.last_post_date DESC';
break;
case 'last_post_date':
default:
$orderBy = 'thread.last_post_date';
}
if (!isset($fetchOptions['orderDirection']) || $fetchOptions['orderDirection'] == 'desc')
{
$orderBy .= ' DESC';
}
else
{
$orderBy .= ' ASC';
}
$orderBy .= $orderBySecondary;
}
if (!empty($fetchOptions['join']))
{
if ($fetchOptions['join'] & self::FETCH_USER)
{
$selectFields .= ',
user.*, IF(user.username IS NULL, thread.username, user.username) AS username';
$joinTables .= '
LEFT JOIN xf_user AS user ON
(user.user_id = thread.user_id)';
}
else if ($fetchOptions['join'] & self::FETCH_AVATAR)
{
$selectFields .= ',
user.gender, user.avatar_date, user.gravatar';
$joinTables .= '
LEFT JOIN xf_user AS user ON
(user.user_id = thread.user_id)';
}
if ($fetchOptions['join'] & self::FETCH_FORUM)
{
$selectFields .= ',
node.title AS node_title, node.node_name';
$joinTables .= '
LEFT JOIN xf_node AS node ON
(node.node_id = thread.node_id)';
}
if ($fetchOptions['join'] & self::FETCH_FORUM_OPTIONS)
{
$selectFields .= ',
forum.*,
forum.last_post_id AS forum_last_post_id,
forum.last_post_date AS forum_last_post_date,
forum.last_post_user_id AS forum_last_post_user_id,
forum.last_post_username AS forum_last_post_username,
forum.last_thread_title AS forum_last_thread_title,
thread.last_post_id,
thread.last_post_date,
thread.last_post_user_id,
thread.last_post_username';
$joinTables .= '
LEFT JOIN xf_forum AS forum ON
(forum.node_id = thread.node_id)';
}
if ($fetchOptions['join'] & self::FETCH_FIRSTPOST)
{
$selectFields .= ',
post.message, post.attach_count';
$joinTables .= '
LEFT JOIN xf_post AS post ON
(post.post_id = thread.first_post_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 = 'thread' AND deletion_log.content_id = thread.thread_id)';
}
}
if (isset($fetchOptions['readUserId']))
{
if (!empty($fetchOptions['readUserId']))
{
$autoReadDate = XenForo_Application::$time - (XenForo_Application::get('options')->readMarkingDataLifetime * 86400);
$joinTables .= '
LEFT JOIN xf_thread_read AS thread_read ON
(thread_read.thread_id = thread.thread_id
AND thread_read.user_id = ' . $this->_getDb()->quote($fetchOptions['readUserId']) . ')';
$joinForumRead = (!empty($fetchOptions['includeForumReadDate'])
|| (!empty($fetchOptions['join']) && $fetchOptions['join'] & self::FETCH_FORUM)
);
if ($joinForumRead)
{
$joinTables .= '
LEFT JOIN xf_forum_read AS forum_read ON
(forum_read.node_id = thread.node_id
AND forum_read.user_id = ' . $this->_getDb()->quote($fetchOptions['readUserId']) . ')';
$selectFields .= ",
GREATEST(COALESCE(thread_read.thread_read_date, 0), COALESCE(forum_read.forum_read_date, 0), $autoReadDate) AS thread_read_date";
}
else
{
$selectFields .= ",
IF(thread_read.thread_read_date > $autoReadDate, thread_read.thread_read_date, $autoReadDate) AS thread_read_date";
}
}
else
{
$selectFields .= ',
NULL AS thread_read_date';
}
}
if (isset($fetchOptions['watchUserId']))
{
if (!empty($fetchOptions['watchUserId']))
{
$selectFields .= ',
IF(thread_watch.user_id IS NULL, 0,
IF(thread_watch.email_subscribe, 'watch_email', 'watch_no_email')) AS thread_is_watched';
$joinTables .= '
LEFT JOIN xf_thread_watch AS thread_watch
ON (thread_watch.thread_id = thread.thread_id
AND thread_watch.user_id = ' . $this->_getDb()->quote($fetchOptions['watchUserId']) . ')';
}
else
{
$selectFields .= ',
0 AS thread_is_watched';
}
}
if (isset($fetchOptions['forumWatchUserId']))
{
if (!empty($fetchOptions['forumWatchUserId']))
{
$selectFields .= ',
IF(forum_watch.user_id IS NULL, 0, 1) AS forum_is_watched';
$joinTables .= '
LEFT JOIN xf_forum_watch AS forum_watch
ON (forum_watch.node_id = thread.node_id
AND forum_watch.user_id = ' . $this->_getDb()->quote($fetchOptions['forumWatchUserId']) . ')';
}
else
{
$selectFields .= ',
0 AS forum_is_watched';
}
}
if (isset($fetchOptions['draftUserId']))
{
if (!empty($fetchOptions['draftUserId']))
{
$selectFields .= ',
draft.message AS draft_message, draft.extra_data AS draft_extra';
$joinTables .= '
LEFT JOIN xf_draft AS draft
ON (draft.draft_key = CONCAT('thread-', thread.thread_id)
AND draft.user_id = ' . $this->_getDb()->quote($fetchOptions['draftUserId']) . ')';
}
else
{
$selectFields .= ',
'' AS draft_message, NULL AS draft_extra';
}
}
if (isset($fetchOptions['postCountUserId']))
{
if (!empty($fetchOptions['postCountUserId']))
{
$selectFields .= ',
thread_user_post.post_count AS user_post_count';
$joinTables .= '
LEFT JOIN xf_thread_user_post AS thread_user_post
ON (thread_user_post.thread_id = thread.thread_id
AND thread_user_post.user_id = ' . $this->_getDb()->quote($fetchOptions['postCountUserId']) . ')';
}
else
{
$selectFields .= ',
0 AS user_post_count';
}
}
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 = ' . $this->_getDb()->quote($fetchOptions['permissionCombinationId']) . '
AND permission.content_type = 'node'
AND permission.content_id = thread.node_id)';
}
return array(
'selectFields' => $selectFields,
'joinTables' => $joinTables,
'orderClause' => ($orderBy ? "ORDER BY $orderBy" : '')
);
}
/**
* Prepares a collection of thread fetching related conditions into an SQL clause
*
* @param array $conditions List of conditions
* @param array $fetchOptions Modifiable set of fetch options (may have joins pushed on to it)
*
* @return string SQL clause (at least 1=1)
*/
public function prepareThreadConditions(array $conditions, array &$fetchOptions)
{
$sqlConditions = array();
$db = $this->_getDb();
if (!empty($conditions['thread_id_gt']))
{
$sqlConditions[] = 'thread.thread_id > ' . $db->quote($conditions['thread_id_gt']);
}
if (!empty($conditions['title']))
{
if (is_array($conditions['title']))
{
$sqlConditions[] = 'thread.title LIKE ' . XenForo_Db::quoteLike($conditions['title'][0], $conditions['title'][1], $db);
}
else
{
$sqlConditions[] = 'thread.title LIKE ' . XenForo_Db::quoteLike($conditions['title'], 'lr', $db);
}
}
if (!empty($conditions['forum_id']) && empty($conditions['node_id']))
{
$conditions['node_id'] = $conditions['forum_id'];
}
if (!empty($conditions['node_id']))
{
if (is_array($conditions['node_id']))
{
$sqlConditions[] = 'thread.node_id IN (' . $db->quote($conditions['node_id']) . ')';
}
else
{
$sqlConditions[] = 'thread.node_id = ' . $db->quote($conditions['node_id']);
}
}
if (!empty($conditions['discussion_type']))
{
if (is_array($conditions['discussion_type']))
{
$sqlConditions[] = 'thread.discussion_type IN (' . $db->quote($conditions['discussion_type']) . ')';
}
else
{
$sqlConditions[] = 'thread.discussion_type = ' . $db->quote($conditions['discussion_type']);
}
}
if (!empty($conditions['not_discussion_type']))
{
if (is_array($conditions['not_discussion_type']))
{
$sqlConditions[] = 'thread.discussion_type NOT IN (' . $db->quote($conditions['not_discussion_type']) . ')';
}
else
{
$sqlConditions[] = 'thread.discussion_type <> ' . $db->quote($conditions['not_discussion_type']);
}
}
if (!empty($conditions['prefix_id']))
{
if (is_array($conditions['prefix_id']))
{
if (in_array(-1, $conditions['prefix_id'])) {
$conditions['prefix_id'][] = 0;
}
$sqlConditions[] = 'thread.prefix_id IN (' . $db->quote($conditions['prefix_id']) . ')';
}
else if ($conditions['prefix_id'] == -1)
{
$sqlConditions[] = 'thread.prefix_id = 0';
}
else
{
$sqlConditions[] = 'thread.prefix_id = ' . $db->quote($conditions['prefix_id']);
}
}
if (isset($conditions['sticky']))
{
$sqlConditions[] = 'thread.sticky = ' . ($conditions['sticky'] ? 1 : 0);
}
if (isset($conditions['discussion_open']))
{
$sqlConditions[] = 'thread.discussion_open = ' . ($conditions['discussion_open'] ? 1 : 0);
}
if (!empty($conditions['discussion_state']))
{
if (is_array($conditions['discussion_state']))
{
$sqlConditions[] = 'thread.discussion_state IN (' . $db->quote($conditions['discussion_state']) . ')';
}
else
{
$sqlConditions[] = 'thread.discussion_state = ' . $db->quote($conditions['discussion_state']);
}
}
if (isset($conditions['deleted']) || isset($conditions['moderated']))
{
$sqlConditions[] = $this->prepareStateLimitFromConditions($conditions, 'thread', 'discussion_state');
}
if (!empty($conditions['last_post_date']) && is_array($conditions['last_post_date']))
{
$sqlConditions[] = $this->getCutOffCondition("thread.last_post_date", $conditions['last_post_date']);
}
if (!empty($conditions['post_date']) && is_array($conditions['post_date']))
{
$sqlConditions[] = $this->getCutOffCondition("thread.post_date", $conditions['post_date']);
}
if (!empty($conditions['reply_count']) && is_array($conditions['reply_count']))
{
$sqlConditions[] = $this->getCutOffCondition("thread.reply_count", $conditions['reply_count']);
}
if (!empty($conditions['first_post_likes']) && is_array($conditions['first_post_likes']))
{
$sqlConditions[] = $this->getCutOffCondition("thread.first_post_likes", $conditions['first_post_likes']);
}
if (!empty($conditions['view_count']) && is_array($conditions['view_count']))
{
$sqlConditions[] = $this->getCutOffCondition("thread.view_count", $conditions['view_count']);
}
// fetch threads only from forums with find_new = 1
if (!empty($conditions['find_new']) && isset($fetchOptions['join']) && $fetchOptions['join'] & self::FETCH_FORUM_OPTIONS)
{
$sqlConditions[] = 'forum.find_new = 1';
}
// thread starter
if (isset($conditions['user_id']))
{
if (is_array($conditions['user_id']))
{
$sqlConditions[] = 'thread.user_id IN (' . $db->quote($conditions['user_id']) . ')';
}
else
{
$sqlConditions[] = 'thread.user_id = ' . $db->quote($conditions['user_id']);
}
}
// watch limit
if (!empty($conditions['watch_only']))
{
$parts = array();
if (!empty($fetchOptions['forumWatchUserId']))
{
$parts[] = 'forum_watch.node_id IS NOT NULL';
}
if (!empty($fetchOptions['watchUserId']))
{
$parts[] = 'thread_watch.thread_id IS NOT NULL';
}
if (!$parts)
{
$sqlConditions[] = '0'; // no watch info - return nothing
}
else
{
$sqlConditions[] = '(' . implode(' OR ', $parts) . ')';
}
}
return $this->getConditionsForClause($sqlConditions);
}
/**
* Gets threads that match the given conditions.
*
* @param array $conditions Conditions to apply to the fetching
* @param array $fetchOptions Collection of options that relate to fetching
*
* @return array Format: [thread id] => info
*/
public function getThreads(array $conditions, array $fetchOptions = array())
{
$whereConditions = $this->prepareThreadConditions($conditions, $fetchOptions);
$sqlClauses = $this->prepareThreadFetchOptions($fetchOptions);
$limitOptions = $this->prepareLimitFetchOptions($fetchOptions);
$forceIndex = (!empty($fetchOptions['forceThreadIndex']) ? 'FORCE INDEX (' . $fetchOptions['forceThreadIndex'] . ')' : '');
return $this->fetchAllKeyed($this->limitQueryResults(
'
SELECT thread.*
' . $sqlClauses['selectFields'] . '
FROM xf_thread AS thread ' . $forceIndex . '
' . $sqlClauses['joinTables'] . '
WHERE ' . $whereConditions . '
' . $sqlClauses['orderClause'] . '
', $limitOptions['limit'], $limitOptions['offset']
), 'thread_id');
}
/**
* Gets thread ids with the specified criteria.
*
* @param array $conditions Conditions to apply to the fetching
* @param array $fetchOptions for limiting only
*
* @return array
*/
public function getThreadIds(array $conditions, array $fetchOptions = array())
{
$fetchOptionsInner = array();
$whereConditions = $this->prepareThreadConditions($conditions, $fetchOptionsInner);
$sqlClauses = $this->prepareThreadFetchOptions($fetchOptionsInner);
$limitOptions = $this->prepareLimitFetchOptions($fetchOptions);
return $this->_getDb()->fetchCol($this->limitQueryResults(
'
SELECT thread.thread_id
FROM xf_thread AS thread
' . $sqlClauses['joinTables'] . '
WHERE ' . $whereConditions . '
ORDER BY thread.thread_id
', $limitOptions['limit'], $limitOptions['offset']
));
}
/**
* Gets the count of threads with the specified criteria.
*
* @param array $conditions Conditions to apply to the fetching
*
* @return integer
*/
public function countThreads(array $conditions)
{
$fetchOptions = array();
$whereConditions = $this->prepareThreadConditions($conditions, $fetchOptions);
$sqlClauses = $this->prepareThreadFetchOptions($fetchOptions);
return $this->_getDb()->fetchOne('
SELECT COUNT(*)
FROM xf_thread AS thread
' . $sqlClauses['joinTables'] . '
WHERE ' . $whereConditions . '
');
}
/**
* Gets an array of the node IDs in which the specified threads reside
*
* @param array $threads
*
* @return array
*/
public function getNodeIdsFromThreads(array $threads)
{
$nodeIds = array();
foreach ($threads AS $thread)
{
$nodeIds[] = $thread['node_id'];
}
return array_unique($nodeIds);
}
/**
* Gets threads that belong to the specified forum.
*
* @param integer $forumId
* @param array $conditions Conditions to apply to the fetching
* @param array $fetchOptions Collection of options that relate to fetching
*
* @return array Format: [thread id] => info
*/
public function getThreadsInForum($forumId, array $conditions = array(), array $fetchOptions = array())
{
$conditions['forum_id'] = $forumId;
return $this->getThreads($conditions, $fetchOptions);
}
/**
* Gets all sticky threads in a particular forum.
*
* @param integer $forumId
* @param array $fetchOptions Collection of options that relate to fetching
*
* @return array Format: [thread id] => info
*/
public function getStickyThreadsInForum($forumId, array $conditions = array(), array $fetchOptions = array())
{
$conditions['forum_id'] = $forumId;
$conditions['sticky'] = 1;
return $this->getThreads($conditions, $fetchOptions);
}
/**
* Gets the count of threads in the specified forum.
*
* @param integer $forumId
* @param array $conditions Conditions to apply to the fetching
*
* @return integer
*/
public function countThreadsInForum($forumId, array $conditions = array())
{
$conditions['forum_id'] = $forumId;
return $this->countThreads($conditions);
}
/**
* Gets the thread with the most recent post in the specified forum.
* Doesn't include redirects.
*
* @param integer $forumId
* @param array $fetchOptions Collection of options that relate to fetching
*
* @return array|false
*/
public function getLastUpdatedThreadInForum($forumId, array $fetchOptions = array())
{
$db = $this->_getDb();
$stateLimit = $this->prepareStateLimitFromConditions($fetchOptions, '', 'discussion_state');
return $db->fetchRow($db->limit('
SELECT *
FROM xf_thread
WHERE node_id = ?
AND discussion_type <> 'redirect'
AND (' . $stateLimit . ')
ORDER BY last_post_date DESC
', 1), $forumId);
}
/**
* Gets thread 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 getThreadIdsInRange($start, $limit)
{
$db = $this->_getDb();
return $db->fetchCol($db->limit('
SELECT thread_id
FROM xf_thread
WHERE thread_id > ?
ORDER BY thread_id
', $limit), $start);
}
/**
* Gets the IDs of threads that the specified user has not read. Doesn't not work for guests.
* Doesn't include deleted.
*
* @param integer $userId
* @param array $fetchOptions Fetching options; limit only
* @param boolean $watchedOnly If true, only returns results in watched threads/forums
*
* @return array List of thread IDs
*/
public function getUnreadThreadIds($userId, array $fetchOptions = array(), $watchedOnly = false)
{
if (!$userId)
{
return array();
}
$limitOptions = $this->prepareLimitFetchOptions($fetchOptions);
$autoReadDate = XenForo_Application::$time - (XenForo_Application::get('options')->readMarkingDataLifetime * 86400);
$userId = intval($userId);
if ($watchedOnly)
{
$joinExtra = '
LEFT JOIN xf_forum_watch AS forum_watch ON (forum_watch.node_id = thread.node_id AND forum_watch.user_id = ' . $userId . ')
LEFT JOIN xf_thread_watch AS thread_watch ON (thread_watch.thread_id = thread.thread_id AND thread_watch.user_id = ' . $userId . ')
';
$whereExtra = 'AND (forum_watch.node_id IS NOT NULL OR thread_watch.thread_id IS NOT NULL)';
}
else
{
$joinExtra = '';
$whereExtra = '';
}
return $this->_getDb()->fetchCol($this->limitQueryResults(
'
SELECT thread.thread_id
FROM xf_thread AS thread
INNER JOIN xf_forum AS forum ON
(forum.node_id = thread.node_id)
LEFT JOIN xf_thread_read AS thread_read ON
(thread_read.thread_id = thread.thread_id AND thread_read.user_id = ?
AND thread_read.thread_read_date >= thread.last_post_date)
LEFT JOIN xf_forum_read AS forum_read ON
(forum_read.node_id = thread.node_id AND forum_read.user_id = ?
AND forum_read.forum_read_date >= thread.last_post_date)
' . $joinExtra . '
WHERE thread_read.thread_read_date IS NULL
AND forum_read.forum_read_date IS NULL
AND thread.last_post_date > ?
AND thread.discussion_type <> 'redirect'
AND thread.discussion_state <> 'deleted'
AND forum.find_new = 1
' . $whereExtra . '
ORDER BY thread.last_post_date DESC
', $limitOptions['limit'], $limitOptions['offset']
), array($userId, $userId, $autoReadDate));
}
/**
* Determines if the thread can be viewed with the given permissions.
* This does not check forum viewing permissions.
*
* @param array $thread Info about the thread
* @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 canViewThread(array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
if (isset($thread['thread_user_id']))
{
$thread['user_id'] = $thread['thread_user_id'];
}
if (!XenForo_Permission::hasContentPermission($nodePermissions, 'view'))
{
return false;
}
if (!XenForo_Permission::hasContentPermission($nodePermissions, 'viewOthers') && ($viewingUser['user_id'] != $thread['user_id'] || !$viewingUser['user_id']))
{
return false;
}
if (!XenForo_Permission::hasContentPermission($nodePermissions, 'viewContent'))
{
// TODO: specific error message?
return false;
}
if ($this->isModerated($thread))
{
if (!XenForo_Permission::hasContentPermission($nodePermissions, 'viewModerated'))
{
if (!$viewingUser['user_id'] || $viewingUser['user_id'] != $thread['user_id'])
{
$errorPhraseKey = 'requested_thread_not_found';
return false;
}
}
}
else if ($this->isDeleted($thread))
{
if (!XenForo_Permission::hasContentPermission($nodePermissions, 'viewDeleted'))
{
$errorPhraseKey = 'requested_thread_not_found';
return false;
}
}
return true;
}
/**
* Determines if the thread can be viewed with the given permissions.
* This will check that any parent container can be viewed as well.
*
* @param array $thread Info about the thread
* @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 canViewThreadAndContainer(array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
if (!$this->_getForumModel()->canViewForum($forum, $errorPhraseKey, $nodePermissions, $viewingUser))
{
return false;
}
return $this->canViewThread($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser);
}
/**
* Checks whether a user can view deleted posts in a thread
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array $nodePermissions
* @param array $viewingUser
*
* @return boolean
*/
public function canViewDeletedPosts(array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
return (XenForo_Permission::hasContentPermission($nodePermissions, 'viewDeleted'));
}
/**
* Checks whether a user can view moderated posts in a thread
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array $nodePermissions
* @param array $viewingUser
*
* @return boolean
*/
public function canViewModeratedPosts(array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
return (XenForo_Permission::hasContentPermission($nodePermissions, 'viewModerated'));
}
/**
* Checks whether a user can view attachments in a thread
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array $nodePermissions
* @param array $viewingUser
*
* @return boolean
*/
public function canViewAttachmentsInThread(array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
return XenForo_Permission::hasContentPermission($nodePermissions, 'viewAttachment');
}
/**
* Determines if a new reply can be posted in the specified thread,
* with the given permissions. This does not check viewing permissions.
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey Returned phrase key for a specific error
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canReplyToThread(array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
if ($this->isRedirect($thread) || $this->isDeleted($thread))
{
return false;
}
if (!$thread['discussion_open'] && !$this->canLockUnlockThread($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser))
{
$errorPhraseKey = 'you_may_not_perform_this_action_because_discussion_is_closed';
return false;
}
if (empty($forum['allow_posting']))
{
$errorPhraseKey = 'you_may_not_perform_this_action_because_forum_does_not_allow_posting';
return false;
}
return XenForo_Permission::hasContentPermission($nodePermissions, 'postReply');
}
/**
* Determines if the thread can be edited with the given permissions.
* This does not check thread viewing permissions.
*
* @param array $thread Info about the thread
* @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 canEditThread(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 thread title be edited with the given permissions.
* This does not check thread viewing permissions.
*
* @param array $thread Info about the thread
* @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 canEditThreadTitle(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->canLockUnlockThread($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser))
{
$errorPhraseKey = 'you_may_not_perform_this_action_because_discussion_is_closed';
return false;
}
if (XenForo_Permission::hasContentPermission($nodePermissions, 'manageAnyThread'))
{
return true;
}
if ($thread['user_id'] == $viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'editOwnPost'))
{
$editLimit = XenForo_Permission::hasContentPermission($nodePermissions, 'editOwnPostTimeLimit');
if ($editLimit != -1 && (!$editLimit || $thread['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 XenForo_Permission::hasContentPermission($nodePermissions, 'editOwnThreadTitle');
}
return false;
}
/**
* Determines if the thread can be locked/unlocked with the given permissions.
* This does not check viewing permissions.
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canLockUnlockThread(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, 'lockUnlockThread'));
}
/**
* Determines if the thread can be stuck/unstuck with the given permissions.
* This does not check viewing permissions.
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canStickUnstickThread(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, 'stickUnstickThread'));
}
/**
* Determines if the thread can be deleted with the given permissions.
* This does not check viewing permissions.
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canDeleteThread(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, 'hardDeleteAnyThread'))
{
// fail immediately on hard delete without permission
return false;
}
if (XenForo_Permission::hasContentPermission($nodePermissions, 'deleteAnyThread'))
{
return true;
}
else if ($thread['user_id'] == $viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'deleteOwnThread'))
{
$editLimit = XenForo_Permission::hasContentPermission($nodePermissions, 'editOwnPostTimeLimit');
if ($editLimit != -1 && (!$editLimit || $thread['post_date'] < XenForo_Application::$time - 60 * $editLimit))
{
$errorPhraseKey = array('message_edit_time_limit_expired', 'minutes' => $editLimit);
return false;
}
if (empty($forum['allow_posting']))
{
return false;
}
return true;
}
return false;
}
/**
* Determines if the thread can be undeleted with the given permissions.
* This does not check thread viewing permissions.
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canUndeleteThread(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 thread moderator log can be viewed.
* This does not check thread viewing permissions.
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canViewThreadModeratorLog(array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
if (!$viewingUser['user_id'])
{
return false;
}
return (
XenForo_Permission::hasContentPermission($nodePermissions, 'manageAnyThread')
|| XenForo_Permission::hasContentPermission($nodePermissions, 'editAnyPost')
|| XenForo_Permission::hasContentPermission($nodePermissions, 'deleteAnyPost')
|| XenForo_Permission::hasContentPermission($nodePermissions, 'deleteAnyThread')
|| XenForo_Permission::hasContentPermission($nodePermissions, 'hardDeleteAnyPost')
|| XenForo_Permission::hasContentPermission($nodePermissions, 'hardDeleteAnyThread')
);
}
/**
* Determines if the thread can be approved/unapproved with the given permissions.
* This does not check thread viewing permissions.
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canApproveUnapproveThread(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 thread can be moved with the given permissions.
* This does not check thread viewing permissions.
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canMoveThread(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 thread can be merged with another with the given permissions.
* This does not check thread viewing permissions.
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canMergeThread(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 thread's discussion_state can be altered to a new value.
* This does not check thread viewing permissions.
*
* @param array $thread
* @param array $forum
* @param string $state (intended new discussion_state)
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canAlterThreadState(array $thread, array $forum, $state, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
if ($state == $thread['discussion_state'])
{
// not attempting to change, so allow
return true;
}
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
switch ($state)
{
case 'visible':
{
if ($this->isModerated($thread)
&& !$this->canApproveUnapproveThread($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser)
)
{
return false;
}
if ($this->isDeleted($thread)
&& !$this->canUndeleteThread($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser)
)
{
return false;
}
break;
}
case 'moderated':
{
if ($this->isVisible($thread)
&& !$this->canApproveUnapproveThread($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser)
)
{
return false;
}
if ($this->isDeleted($thread) &&
(
!$this->canUndeleteThread($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser)
||
!$this->canApproveUnapproveThread($thread, $forum, $errorPhraseKey, $nodePermissions, $viewingUser)
)
)
{
return false;
}
break;
}
case 'deleted':
{
if (!$this->canDeleteThread($thread, $forum, 'soft', $errorPhraseKey, $nodePermissions, $viewingUser))
{
return false;
}
break;
}
default:
{
return false;
}
}
return true;
}
/**
* Determines if the thread can be watched with the given permissions.
* This does not check thread viewing permissions.
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canWatchThread(array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
return ($viewingUser['user_id'] ? true : false);
}
/**
* Determines if the poll in the given thread can be voted on. This does not
* check if the user has already voted on the poll.
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canVoteOnPoll(array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
if (!$thread['discussion_open'])
{
$errorPhraseKey = 'you_may_not_perform_this_action_because_discussion_is_closed';
return false;
}
if (empty($forum['allow_posting']))
{
$errorPhraseKey = 'you_may_not_perform_this_action_because_forum_does_not_allow_posting';
return false;
}
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
return ($viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'votePoll'));
}
/**
* Determines if the poll in the given thread can be edited.
* This does not check thread viewing permissions.
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canEditPoll(array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
// TODO: allow limited editing by starter (no votes, can add new responses, etc)
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
return ($viewingUser['user_id'] && XenForo_Permission::hasContentPermission($nodePermissions, 'manageAnyThread'));
}
/**
* Determines if the specified user can view IP addresses
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canViewIps(array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
return $this->getModelFromCache('XenForo_Model_User')->canViewIps($errorPhraseKey, $viewingUser);
}
/**
* Determines if a user can reply to the thread using Quick Reply.
* Note that this always assumes the visitor!
*
* @param array $thread
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
*
* @return boolean
*/
public function canQuickReply(array $thread, array $forum, &$errorPhraseKey = '', array $nodePermissions = null)
{
if (!$this->canReplyToThread($thread, $forum, $errorPhraseKey, $nodePermissions))
{
return false;
}
$visitor = XenForo_Visitor::getInstance();
if (!$visitor['user_id'] || $visitor->showCaptcha())
{
return false;
}
else
{
return true;
}
}
/**
* Adds the canInlineMod value to the provided thread and returns the
* specific list of inline mod actions that are allowed on this thread.
*
* @param array $thread Thread info
* @param array $forum Forum the thread is in
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return array List of allowed inline mod actions, format: [action] => true
*/
public function addInlineModOptionToThread(array &$thread, array $forum, array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
$modOptions = array();
$canInlineMod = ($viewingUser['user_id'] && (
XenForo_Permission::hasContentPermission($nodePermissions, 'deleteAnyThread')
|| XenForo_Permission::hasContentPermission($nodePermissions, 'undelete')
|| XenForo_Permission::hasContentPermission($nodePermissions, 'approveUnapprove')
|| XenForo_Permission::hasContentPermission($nodePermissions, 'lockUnlockThread')
|| XenForo_Permission::hasContentPermission($nodePermissions, 'stickUnstickThread')
|| XenForo_Permission::hasContentPermission($nodePermissions, 'manageAnyThread')
));
if ($canInlineMod)
{
if ($this->canDeleteThread($thread, $forum, 'soft', $null, $nodePermissions, $viewingUser))
{
$modOptions['delete'] = true;
}
if ($this->canUndeleteThread($thread, $forum, $null, $nodePermissions, $viewingUser))
{
$modOptions['undelete'] = true;
}
if ($this->canApproveUnapproveThread($thread, $forum, $null, $nodePermissions, $viewingUser))
{
$modOptions['approve'] = true;
$modOptions['unapprove'] = true;
}
if ($this->canLockUnlockThread($thread, $forum, $null, $nodePermissions, $viewingUser))
{
$modOptions['lock'] = true;
$modOptions['unlock'] = true;
}
if ($this->canStickUnstickThread($thread, $forum, $null, $nodePermissions, $viewingUser))
{
$modOptions['stick'] = true;
$modOptions['unstick'] = true;
}
if ($this->canMoveThread($thread, $forum, $null, $nodePermissions, $viewingUser))
{
$modOptions['move'] = true;
}
if ($this->canMergeThread($thread, $forum, $null, $nodePermissions, $viewingUser))
{
$modOptions['merge'] = true;
}
if ($this->canEditThread($thread, $forum, $null, $nodePermissions, $viewingUser))
{
$modOptions['edit'] = true;
}
}
$thread['canInlineMod'] = (count($modOptions) > 0);
return $modOptions;
}
/**
* Prepares a thread for display, generally within the context of a specific forum.
*
* @param array $thread Thread to prepare
* @param array $forum Forum thread is in
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return array Prepared version of thread
*/
public function prepareThread(array $thread, array $forum, array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
$thread['lastPostInfo'] = array(
'post_date' => $thread['last_post_date'],
'post_id' => $thread['last_post_id'],
'user_id' => $thread['last_post_user_id'],
'username' => $thread['last_post_username']
);
if (isset($thread['node_title']))
{
$thread['forum'] = array(
'node_id' => $thread['node_id'],
'title' => $thread['node_title'],
'node_name' => isset($thread['node_name']) ? $thread['node_name'] : null
);
}
if ($thread['view_count'] <= $thread['reply_count'])
{
$thread['view_count'] = $thread['reply_count'] + 1;
}
if (!empty($thread['delete_date']))
{
$thread['deleteInfo'] = array(
'user_id' => $thread['delete_user_id'],
'username' => $thread['delete_username'],
'date' => $thread['delete_date'],
'reason' => $thread['delete_reason'],
);
}
if (!isset($thread['canInlineMod']))
{
$this->addInlineModOptionToThread($thread, $forum, $nodePermissions, $viewingUser);
}
$thread['canEditThread'] = $this->canEditThread($thread, $forum, $_null, $nodePermissions, $viewingUser);
$thread['isNew'] = $this->isNew($thread, $forum);
if ($thread['isNew'])
{
$readDate = $this->getMaxThreadReadDate($thread, $forum);
$thread['haveReadData'] = ($readDate > XenForo_Application::$time - (XenForo_Application::get('options')->readMarkingDataLifetime * 86400));
}
else
{
$thread['haveReadData'] = false;
}
$thread['hasPreview'] = $this->hasPreview($thread, $forum, $nodePermissions, $viewingUser);
$thread['canViewContent'] = $this->_getForumModel()->canViewForumContent($forum, $null, $nodePermissions, $viewingUser);
$thread['isRedirect'] = $this->isRedirect($thread);
$thread['isDeleted'] = $this->isDeleted($thread);
$thread['isModerated'] = $this->isModerated($thread);
$thread['title'] = XenForo_Helper_String::censorString($thread['title']);
$thread['titleCensored'] = true;
$thread['lastPageNumbers'] = $this->getLastPageNumbers($thread['reply_count']);
if (array_key_exists('user_group_id', $thread))
{
$thread = $this->getModelFromCache('XenForo_Model_User')->prepareUser($thread);
}
return $thread;
}
/**
* Determines if a thread is a redirect (based on discussion_type)
*
* @param array $thread
*
* @return boolean
*/
public function isRedirect(array $thread)
{
return ($thread['discussion_type'] == 'redirect');
}
/**
* Determines if a thread is deleted (based on discussion_state)
*
* @param array $thread
*
* @return boolean
*/
public function isDeleted(array $thread)
{
return ($thread['discussion_state'] == 'deleted');
}
/**
* Determines if a thread is moderated (based on discussion_state)
*
* @param array $thread
*
* @return boolean
*/
public function isModerated(array $thread)
{
return ($thread['discussion_state'] == 'moderated');
}
/**
* Determines if a thread is visible (based on discussion_state)
*
* @param array $thread
*
* @return boolean
*/
public function isVisible(array $thread)
{
return ($thread['discussion_state'] == 'visible');
}
/**
* Determines if a thread is new / unread
*
* @param array $thread (expects thread_read_date or forum_read_date, discussion_type and last_post_date indeces)
*
* @return boolean
*/
public function isNew(array $thread, array $forum)
{
if (isset($thread['thread_read_date']) || isset($forum['forum_read_date']))
{
if ($this->isRedirect($thread) || $this->isDeleted($thread))
{
return false;
}
else
{
return ($this->getMaxThreadReadDate($thread, $forum) < $thread['last_post_date']);
}
}
return false;
}
/**
* Determines if a thread can have a thread preview
*
* @param array $thread (expects first_post_id and discussion_type indeces)
* @param array $forum
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function hasPreview(array $thread, array $forum, array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($thread['node_id'], $viewingUser, $nodePermissions);
return
(
$thread['first_post_id']
&& XenForo_Application::get('options')->discussionPreviewLength
&& $this->isRedirect($thread) == false
&& XenForo_Permission::hasContentPermission($nodePermissions, 'viewContent')
);
}
/**
* Gets permission-based conditions that apply to thread fetching functions.
*
* @param array $forum Forum the threads will belong to
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return array Keys: deleted (boolean), moderated (boolean or integer, if can only view single user's)
*/
public function getPermissionBasedThreadFetchConditions(array $forum, array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($forum['node_id'], $viewingUser, $nodePermissions);
if (XenForo_Permission::hasContentPermission($nodePermissions, 'viewModerated'))
{
$viewModerated = true;
}
else if ($viewingUser['user_id'])
{
$viewModerated = $viewingUser['user_id'];
}
else
{
$viewModerated = false;
}
$conditions = array(
'deleted' => XenForo_Permission::hasContentPermission($nodePermissions, 'viewDeleted'),
'moderated' => $viewModerated
);
if (!XenForo_Permission::hasContentPermission($nodePermissions, 'viewOthers'))
{
$conditions['user_id'] = $viewingUser['user_id'] ? $viewingUser['user_id'] : -1;
}
return $conditions;
}
/**
* Helper to delete the specified thread, via a soft or hard delete.
*
* @param integer $threadId ID of the thread to delete
* @param string $deleteType Type of deletion (soft or hard)
* @param array $options Deletion options. Currently unused.
*
* @return XenForo_DataWriter_Discussion_Thread The DW used to delete the thread
*/
public function deleteThread($threadId, $deleteType, array $options = array())
{
$options = array_merge(array(
'reason' => ''
), $options);
$dw = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread');
$dw->setExistingData($threadId);
if ($deleteType == 'hard')
{
$dw->delete();
}
else
{
$dw->setExtraData(XenForo_DataWriter_Discussion::DATA_DELETE_REASON, $options['reason']);
$dw->set('discussion_state', 'deleted');
$dw->save();
}
return $dw;
}
/**
* Rebuilds the thread user post counters for a specific thread. If a user ID is specified,
* the counters are only updated for that user.
*
* @param integer $threadId
* @param integer|null $userId
*/
public function rebuildThreadUserPostCounters($threadId, $userId = null)
{
if ($userId === 0)
{
return;
}
$db = $this->_getDb();
$records = $db->fetchPairs('
SELECT user_id, COUNT(*)
FROM xf_post
WHERE thread_id = ?
AND message_state = 'visible'
' . ($userId !== null ? ' AND user_id = ' . $db->quote($userId) : '') . '
GROUP BY user_id
', $threadId);
$this->replaceThreadUserPostCounters($threadId, $records, $userId);
}
/**
* Replaces the thread counters in a specified thread with the given set.
* Old post count records are removed. If a user ID is not null, only that
* user's post count record is removed, so the array is must only contain
* records for that user.
*
* @param integer $threadId
* @param array $counters [user id] => post count
* @param integer|null $userId
*/
public function replaceThreadUserPostCounters($threadId, array $counters, $userId = null)
{
$db = $this->_getDb();
XenForo_Db::beginTransaction($db);
$userIdConstraint = ($userId !== null ? ' AND user_id = ' . $db->quote($userId) : ' AND 1=1');
$db->delete('xf_thread_user_post', 'thread_id = ' . $db->quote($threadId) . $userIdConstraint);
foreach ($counters AS $userId => $count)
{
if (!$userId)
{
continue;
}
$db->insert('xf_thread_user_post', array(
'thread_id' => $threadId,
'user_id' => $userId,
'post_count' => $count
));
}
XenForo_Db::commit($db);
}
/**
* Modifies the count of posts a user has made in a thread.
*
* @param integer $threadId
* @param integer $userId
* @param integer $modifyValue How to modify the count (eg, 1 or -1)
*/
public function modifyThreadUserPostCount($threadId, $userId, $modifyValue)
{
$userId = intval($userId);
if (!$userId)
{
return false;
}
$db = $this->_getDb();
$postCount = $db->fetchOne('
SELECT post_count
FROM xf_thread_user_post
WHERE thread_id = ?
AND user_id = ?
', array($threadId, $userId));
if (!$modifyValue)
{
return $postCount;
}
if ($postCount === false)
{
// insert
if ($modifyValue < 0)
{
return false;
}
$db->insert('xf_thread_user_post', array(
'thread_id' => $threadId,
'user_id' => $userId,
'post_count' => $modifyValue
));
return $modifyValue;
}
else
{
// update
$finalValue = $postCount + $modifyValue;
$condition = 'thread_id = ' . $db->quote($threadId) . ' AND user_id = ' . $db->quote($userId);
if ($finalValue <= 0)
{
$db->delete('xf_thread_user_post', $condition);
return false;
}
else
{
$db->update('xf_thread_user_post', array('post_count' => $finalValue), $condition);
return $finalValue;
}
}
}
/**
* From a list of thread IDs, gets info about the threads and
* the forums the threads are in.
*
* If a permission combination ID is passed, the forums will retrieve permission info.
*
* @param array $threadIds List of thread Ids
* @param integer $permissionCombinationId Permission combination ID that will be retrieved with the forums, into nodePermissions.
*
* @return array Format: [0] => list of threads, [1] => list of forums
*/
public function getThreadsAndParentData(array $threadIds, $permissionCombinationId = null)
{
if ($permissionCombinationId === null)
{
$visitor = XenForo_Visitor::getInstance();
$permissionCombinationId = $visitor['permission_combination_id'];
}
$forums = array();
$threads = $this->getThreadsByIds($threadIds);
if ($threads)
{
$forumIds = array();
foreach ($threads AS $thread)
{
$forumIds[] = $thread['node_id'];
}
$forums = $this->_getForumModel()->getForumsByIds(
$forumIds, array('permissionCombinationId' => $permissionCombinationId)
);
foreach ($forums AS &$forum)
{
$forum['nodePermissions'] = isset($forum['node_permission_cache'])
? XenForo_Permission::unserializePermissions($forum['node_permission_cache'])
: array()
;
}
foreach ($threads AS $threadId => $thread)
{
if (!isset($forums[$thread['node_id']]))
{
unset($threads[$threadId]);
}
}
}
return array($threads, $forums);
}
/**
* Marks the given thread as read up to a certain point (usually the most recent post read).
* Thread must have thread_read_date key. (Forum should have forum_read_date key.)
*
* @param array $thread Thread info
* @param array $forum Forum info
* @param integer $readDate Timestamp to mark
* @param array|null $viewingUser
*
* @return boolean True if marked as read
*/
public function markThreadRead(array $thread, array $forum, $readDate, array $viewingUser = null)
{
$this->standardizeViewingUserReference($viewingUser);
$userId = $viewingUser['user_id'];
if (!$userId)
{
return false;
}
if (!array_key_exists('thread_read_date', $thread))
{
$thread['thread_read_date'] = $this->getUserThreadReadDate($userId, $thread['thread_id']);
}
if ($readDate <= $this->getMaxThreadReadDate($thread, $forum))
{
return false;
}
$this->_getDb()->query('
INSERT INTO xf_thread_read
(user_id, thread_id, thread_read_date)
VALUES
(?, ?, ?)
ON DUPLICATE KEY UPDATE thread_read_date = VALUES(thread_read_date)
', array($userId, $thread['thread_id'], $readDate));
if ($readDate < $thread['last_post_date'])
{
// we haven't finished reading this thread - forum won't be read
return false;
}
$this->_getForumModel()->markForumReadIfNeeded($forum, $viewingUser);
return true;
}
/**
* Get the time when a user has marked the given thread as read.
*
* @param integer $userId
* @param integer $threadId
*
* @return integer|null Null if guest; timestamp otherwise
*/
public function getUserThreadReadDate($userId, $threadId)
{
if (!$userId)
{
return null;
}
$readDate = $this->_getDb()->fetchOne('
SELECT thread_read_date
FROM xf_thread_read
WHERE user_id = ?
AND thread_id = ?
', array($userId, $threadId));
$autoReadDate = XenForo_Application::$time - (XenForo_Application::get('options')->readMarkingDataLifetime * 86400);
return max($readDate, $autoReadDate);
}
/**
* Get the maximum thread read timestamp based on when the thread/forum has been read.
*
* @param array $thread
* @param array $forum
*
* @return integer Read timestamp (may be 0)
*/
public function getMaxThreadReadDate(array $thread, array $forum)
{
$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']; }
return max($readOptions);
}
/**
* Merge multiple threads into a single thread
*
* @param array $threads
* @param integer $targetThreadId
* @param array $options
*
* @return boolean|array False if failure, otherwise thread array of merged thread
*/
public function mergeThreads(array $threads, $targetThreadId, array $options = array())
{
if (!isset($threads[$targetThreadId]))
{
return false;
}
$targetThread = $threads[$targetThreadId];
unset($threads[$targetThreadId]);
$mergeFromThreadIds = array_keys($threads);
if (!$mergeFromThreadIds)
{
return false;
}
$options = array_merge(
array(
'redirect' => false,
'redirectExpiry' => 0,
'log' => true
),
$options
);
$postModel = $this->_getPostModel();
$db = $this->_getDb();
$movePosts = $this->fetchAllKeyed('
SELECT post_id, thread_id, user_id, message_state
FROM xf_post
WHERE thread_id IN (' . $db->quote($mergeFromThreadIds) . ')
', 'post_id');
$movePostIds = array_keys($movePosts);
XenForo_Db::beginTransaction($db);
$idsQuoted = $db->quote($mergeFromThreadIds);
$db->update('xf_post',
array('thread_id' => $targetThreadId),
'post_id IN (' . $db->quote($movePostIds) . ')'
);
$db->query("
UPDATE IGNORE xf_thread_watch SET
thread_id = ?
WHERE thread_id IN ($idsQuoted)
", array($targetThreadId));
$newCounters = $postModel->recalculatePostPositionsInThread($targetThreadId);
if (!$newCounters['firstPostId'])
{
XenForo_Db::rollback($db);
return false;
}
// TODO: user message counts will go off if merging from a visible thread into a hidden one or vice versa
// TODO: user message counts will also go off if merging from a thread in a counting forum into a non-counting forum, or vice versa
$threadDw = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread');
$threadDw->setExistingData($targetThreadId);
$threadDw->rebuildDiscussionCounters(
$newCounters['visibleCount'] - 1, $newCounters['firstPostId'], $newCounters['lastPostId']
);
$viewCount = $threadDw->get('view_count');
foreach ($threads AS $thread)
{
$viewCount += $thread['view_count'];
}
$threadDw->set('view_count', $viewCount);
$threadDw->save();
$targetThread = $threadDw->getMergedData();
if ($options['redirect'])
{
$targetUrl = XenForo_Link::buildPublicLink('threads', $targetThread);
$redirectKey = "thread-$targetThread[thread_id]-";
foreach ($threads AS $thread)
{
$redirectDw = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread');
$redirectDw->setExistingData($thread, true);
$redirectDw->set('discussion_type', 'redirect');
$redirectDw->save();
$this->getModelFromCache('XenForo_Model_ThreadRedirect')->insertThreadRedirect(
$thread['thread_id'], $targetUrl, $redirectKey, $options['redirectExpiry']
);
}
$db->delete('xf_thread_watch', "thread_id IN ($idsQuoted)");
$db->delete('xf_thread_user_post', "thread_id IN ($idsQuoted)");
}
else
{
foreach ($threads AS $thread)
{
$deleteDw = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread');
$deleteDw->setExistingData($thread, true);
$deleteDw->delete();
}
}
$forumIds = array();
foreach ($threads AS $thread)
{
$forumIds[$thread['node_id']] = $thread['node_id'];
}
foreach ($forumIds AS $forumId)
{
$forumDw = XenForo_DataWriter::create('XenForo_DataWriter_Forum', XenForo_DataWriter::ERROR_SILENT);
$forumDw->setExistingData($forumId);
$forumDw->rebuildCounters();
$forumDw->save();
}
$this->replaceThreadUserPostCounters($targetThreadId, $newCounters['userPosts']);
XenForo_Application::defer('SearchIndexPartial', array('contentType' => 'post', 'contentIds' => $movePostIds));
if ($options['log'])
{
XenForo_Model_Log::logModeratorAction(
'thread', $targetThread, 'merge_target', array('ids' => implode(', ', array_keys($threads)))
);
}
XenForo_Db::commit($db);
return $targetThread;
}
/**
* Logs the viewing of a thread.
*
* @param integer $threadId
*/
public function logThreadView($threadId)
{
$this->_getDb()->query('
INSERT ' . (XenForo_Application::get('options')->enableInsertDelayed ? 'DELAYED' : '') . ' INTO xf_thread_view
(thread_id)
VALUES
(?)
', $threadId);
}
/**
* Updates thread views in bulk.
*/
public function updateThreadViews()
{
$db = $this->_getDb();
$db->query('
UPDATE xf_thread
INNER JOIN (
SELECT thread_id, COUNT(*) AS total
FROM xf_thread_view
GROUP BY thread_id
) AS xf_tv ON (xf_tv.thread_id = xf_thread.thread_id)
SET xf_thread.view_count = xf_thread.view_count + xf_tv.total
');
$db->query('TRUNCATE TABLE xf_thread_view');
}
/**
* Returns the last few page numbers of a thread
*
* @param integer $replyCount
*
* @return array|boolean
*/
public function getLastPageNumbers($replyCount)
{
$perPage = XenForo_Application::get('options')->messagesPerPage;
if (($replyCount +1) > $perPage)
{
return XenForo_Helper_Discussion::getLastPageNumbers($replyCount, $perPage);
}
else
{
return false;
}
}
/**
* Use the 'thread_is_watched' info in a thread array to get the watch state text
*
* @param array $thread
* @param boolean $useDefaultIfNotWatching
* @param array $viewingUser
*
* @return string
*/
public function getThreadWatchStateFromThread(array $thread, $useDefaultIfNotWatching = true, array $viewingUser = null)
{
$this->standardizeViewingUserReference($viewingUser);
if (!empty($thread['thread_is_watched']))
{
return $thread['thread_is_watched'];
}
else if ($useDefaultIfNotWatching)
{
return $viewingUser['default_watch_state'];
}
else
{
return '';
}
}
/**
* Gets the date of the earliest posted thread in the database
*
* @return integer
*/
public function getEarliestThreadDate()
{
return (int)$this->_getDb()->fetchOne('SELECT MIN(post_date) FROM xf_thread');
}
/**
* @return XenForo_Model_Forum
*/
protected function _getForumModel()
{
return $this->getModelFromCache('XenForo_Model_Forum');
}
/**
* @return XenForo_Model_Post
*/
protected function _getPostModel()
{
return $this->getModelFromCache('XenForo_Model_Post');
}
}