Файл: library/XenForo/Model/Forum.php
Строк: 923
<?php
/**
* Model for forums
*
* @package XenForo_Forum
*/
class XenForo_Model_Forum extends XenForo_Model
{
/**
* Fetches the combined node-forum record for the specified node id
*
* @param integer $id Node ID
* @param array $fetchOptions Options that affect what is fetched
*
* @return array
*/
public function getForumById($id, array $fetchOptions = array())
{
$joinOptions = $this->prepareForumJoinOptions($fetchOptions);
return $this->_getDb()->fetchRow('
SELECT node.*, forum.*
' . $joinOptions['selectFields'] . '
FROM xf_forum AS forum
INNER JOIN xf_node AS node ON (node.node_id = forum.node_id)
' . $joinOptions['joinTables'] . '
WHERE node.node_id = ?
', $id);
}
/**
* Fetches the combined node-forum record for the specified node name
*
* @param string $name Node name
* @param array $fetchOptions Options that affect what is fetched
*
* @return array
*/
public function getForumByNodeName($name, array $fetchOptions = array())
{
$joinOptions = $this->prepareForumJoinOptions($fetchOptions);
return $this->_getDb()->fetchRow('
SELECT node.*, forum.*
' . $joinOptions['selectFields'] . '
FROM xf_forum AS forum
INNER JOIN xf_node AS node ON (node.node_id = forum.node_id)
' . $joinOptions['joinTables'] . '
WHERE node.node_name = ?
AND node.node_type_id = 'Forum'
', $name);
}
/**
* Fetches the combined node-forum records for the specified forum/node IDs.
*
* @param array $forumIds
* @param array $fetchOptions Options that affect what is fetched
*
* @return array Format: [node id] => info
*/
public function getForumsByIds(array $forumIds, array $fetchOptions = array())
{
if (!$forumIds)
{
return array();
}
$joinOptions = $this->prepareForumJoinOptions($fetchOptions);
return $this->fetchAllKeyed('
SELECT node.*, forum.*
' . $joinOptions['selectFields'] . '
FROM xf_forum AS forum
INNER JOIN xf_node AS node ON (node.node_id = forum.node_id)
' . $joinOptions['joinTables'] . '
WHERE node.node_id IN (' . $this->_getDb()->quote($forumIds) . ')
', 'node_id');
}
/**
* Gets all forums matching the specified criteria (no criteria implemented yet).
*
* @param array $conditions
* @param array $fetchOptions
*
* @return array
*/
public function getForums(array $conditions = array(), array $fetchOptions = array())
{
$whereConditions = $this->prepareForumConditions($conditions, $fetchOptions);
$joinOptions = $this->prepareForumJoinOptions($fetchOptions);
$limitOptions = $this->prepareLimitFetchOptions($fetchOptions);
return $this->fetchAllKeyed($this->limitQueryResults(
'
SELECT node.*, forum.*
' . $joinOptions['selectFields'] . '
FROM xf_forum AS forum
INNER JOIN xf_node AS node ON (node.node_id = forum.node_id)
' . $joinOptions['joinTables'] . '
WHERE ' . $whereConditions . '
', $limitOptions['limit'], $limitOptions['offset']
), 'node_id');
}
/**
* Gets the extra data that applies to the specified forum nodes.
*
* @param array $nodeIds
* @param array $fetchOptions Options that affect what is fetched
*
* @return array Format: [node id] => extra info
*/
public function getExtraForumDataForNodes(array $nodeIds, array $fetchOptions = array())
{
if (!$nodeIds)
{
return array();
}
$joinOptions = $this->prepareForumJoinOptions($fetchOptions);
return $this->fetchAllKeyed('
SELECT forum.*
' . $joinOptions['selectFields'] . '
FROM xf_forum AS forum
INNER JOIN xf_node AS node ON (node.node_id = forum.node_id)
' . $joinOptions['joinTables'] . '
WHERE forum.node_id IN (' . $this->_getDb()->quote($nodeIds) . ')
', 'node_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 Array containing a 'join' integer key build from this class's FETCH_x bitfields and other keys
*
* @return array Containing 'selectFields' and 'joinTables' keys. Example: selectFields = ', user.*, foo.title'; joinTables = ' INNER JOIN foo ON (foo.id = other.id) '
*/
public function prepareForumJoinOptions(array $fetchOptions)
{
$selectFields = '';
$joinTables = '';
$db = $this->_getDb();
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 = forum.node_id)';
}
if (isset($fetchOptions['readUserId']))
{
if (!empty($fetchOptions['readUserId']))
{
$autoReadDate = XenForo_Application::$time - (XenForo_Application::get('options')->readMarkingDataLifetime * 86400);
$selectFields .= ",
IF(forum_read.forum_read_date > $autoReadDate, forum_read.forum_read_date, $autoReadDate) AS forum_read_date";
$joinTables .= '
LEFT JOIN xf_forum_read AS forum_read ON
(forum_read.node_id = forum.node_id
AND forum_read.user_id = ' . $db->quote($fetchOptions['readUserId']) . ')';
}
else
{
$selectFields .= ',
NULL AS forum_read_date';
}
}
if (isset($fetchOptions['watchUserId']))
{
if (!empty($fetchOptions['watchUserId']))
{
$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 = forum.node_id
AND forum_watch.user_id = ' . $this->_getDb()->quote($fetchOptions['watchUserId']) . ')';
}
else
{
$selectFields .= ',
0 AS forum_is_watched';
}
}
if (isset($fetchOptions['threadId']))
{
$joinTables .= '
INNER JOIN xf_thread AS thread ON
(thread.node_id = forum.node_id)';
}
return array(
'selectFields' => $selectFields,
'joinTables' => $joinTables
);
}
/**
* Prepares a collection of forum 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 prepareForumConditions(array $conditions, array &$fetchOptions)
{
$sqlConditions = array();
$db = $this->_getDb();
// thread id
if (isset($conditions['thread_id']))
{
$fetchOptions['threadId'] = true;
$fetchOptions['limit'] = 1;
$sqlConditions[] = 'thread.thread_id = ' . $db->quote($conditions['thread_id']);
}
if (isset($conditions['thread_ids']))
{
$fetchOptions['threadId'] = true;
$sqlConditions[] = 'thread.thread_id IN(' . $db->quote($conditions['thread_ids']) . ')';
}
return $this->getConditionsForClause($sqlConditions);
}
/**
* Prepares a forum for display.
*
* @param array $forum Unprepared forum
*
* @return array Prepared forum
*/
public function prepareForum(array $forum)
{
$forum['hasNew'] = (isset($forum['forum_read_date']) && $forum['forum_read_date'] < $forum['last_post_date']);
$forum['prefixCache'] = (!empty($forum['prefix_cache']) ? unserialize($forum['prefix_cache']) : array());
return $forum;
}
/**
* Prepares a collection of forums for display.
*
* @param array $forums Unprepared forums
*
* @return array Prepared forums
*/
public function prepareForums(array $forums)
{
foreach ($forums AS &$forum)
{
$forum = $this->prepareForum($forum);
}
return $forums;
}
/**
* Gets the permissions in use for a specific forum from the details of
* the forum, or from a permissions override list if provided.
*
* When looking within the forum, looks for "nodePermissions" or "node_permission_cache" keys.
*
* @param array $forum Forum info
* @param array $permissionsList Optional permissions to override; format: [forum id] => permissions
*
* @return array Permissions for forum
*/
public function getPermissionsForForum(array $forum, array $permissionsList = array())
{
if (isset($permissionsList[$forum['node_id']]))
{
return $permissionsList[$forum['node_id']];
}
else if (isset($forum['nodePermissions']))
{
return $forum['nodePermissions'];
}
else if (isset($forum['node_permission_cache']))
{
return XenForo_Permission::unserializePermissions($forum['node_permission_cache']);
}
else
{
return array();
}
}
/**
* Determines if the specified forum can be viewed with the given permissions.
*
* @param array $forum Info about the forum posting in
* @param string $errorPhraseKey Returned phrase key for a specific error
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canViewForum(array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($forum['node_id'], $viewingUser, $nodePermissions);
return XenForo_Permission::hasContentPermission($nodePermissions, 'view');
}
/**
* Determines if the specified forum can be viewed with the given permissions.
* Does not check viewing permissions.
*
* @param array $forum Info about the forum
* @param string $errorPhraseKey Returned phrase key for a specific error
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canViewForumContent(array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($forum['node_id'], $viewingUser, $nodePermissions);
return XenForo_Permission::hasContentPermission($nodePermissions, 'viewContent');
}
/**
* Determines if a new thread can be posted in the specified forum,
* with the given permissions. If no permissions are specified, permissions
* are retrieved from the currently visiting user. This does not check viewing permissions.
*
* @param array $forum Info about the forum posting in
* @param string $errorPhraseKey Returned phrase key for a specific error
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canPostThreadInForum(array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($forum['node_id'], $viewingUser, $nodePermissions);
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, 'postThread');
}
/**
* Determines if a new attachment can be posted in the specified forum,
* with the given permissions. If no permissions are specified, permissions
* are retrieved from the currently visiting user. This does not check viewing permissions.
*
* @param array $forum Info about the forum posting in
* @param string $errorPhraseKey Returned phrase key for a specific error
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canUploadAndManageAttachment(array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($forum['node_id'], $viewingUser, $nodePermissions);
if (!$viewingUser['user_id'])
{
return false;
}
return XenForo_Permission::hasContentPermission($nodePermissions, 'uploadAttachment');
}
/**
* Determines if a thread can be locked or unlocked in the specified forum
* with the given permissions.
*
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canLockUnlockThreadInForum(array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($forum['node_id'], $viewingUser, $nodePermissions);
if (!$viewingUser['user_id'])
{
return false;
}
return XenForo_Permission::hasContentPermission($nodePermissions, 'lockUnlockThread');
}
/**
* Determines if a thread can be stuck or unstuck in the specified forum
* with the given permissions.
*
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canStickUnstickThreadInForum(array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($forum['node_id'], $viewingUser, $nodePermissions);
if (!$viewingUser['user_id'])
{
return false;
}
return XenForo_Permission::hasContentPermission($nodePermissions, 'stickUnstickThread');
}
/**
* Determines if the forum can be watched with the given permissions.
* This does not check forum viewing permissions.
*
* @param array $forum
* @param string $errorPhraseKey
* @param array|null $nodePermissions
* @param array|null $viewingUser
*
* @return boolean
*/
public function canWatchForum(array $forum, &$errorPhraseKey = '', array $nodePermissions = null, array $viewingUser = null)
{
$this->standardizeViewingUserReferenceForNode($forum['node_id'], $viewingUser, $nodePermissions);
return ($viewingUser['user_id'] ? true : false);
}
/**
* Gets the set of attachment params required to allow uploading.
*
* @param array $forum
* @param array $contentData Information about the content, for URL building
* @param array|null $nodePermissions
* @param array|null $viewingUser
* @param string|null $tempHash
*
* @return array|false
*/
public function getAttachmentParams(array $forum, array $contentData, array $nodePermissions = null, array $viewingUser = null, $tempHash = null)
{
if ($this->canUploadAndManageAttachment($forum, $null, $nodePermissions, $viewingUser))
{
$existing = is_string($tempHash) && strlen($tempHash) == 32;
$output = array(
'hash' => $existing ? $tempHash : md5(uniqid('', true)),
'content_type' => 'post',
'content_data' => $contentData
);
if ($existing)
{
$attachmentModel = $this->getModelFromCache('XenForo_Model_Attachment');
$output['attachments'] = $attachmentModel->prepareAttachments(
$attachmentModel->getAttachmentsByTempHash($tempHash)
);
}
return $output;
}
else
{
return false;
}
}
/**
* Gets the count of unread threads in the given forum. This only applies to registered
* users. If no user ID is given, false is returned.
*
* @param integer $forumId
* @param integer $userId
* @param integer $forumReadDate Time when the whole forum is read from
* @param string|array $ignored List of ignored user IDs; if string, unserialized and keys used
*
* @return integer|false
*/
public function getUnreadThreadCountInForum($forumId, $userId, $forumReadDate = 0, $ignored = false)
{
if (!$userId)
{
return false;
}
if ($ignored && is_string($ignored))
{
$ignored = unserialize($ignored);
$ignored = array_keys($ignored);
}
$db = $this->_getDb();
return $db->fetchOne('
SELECT COUNT(*)
FROM xf_thread AS thread
LEFT JOIN xf_thread_read AS thread_read ON
(thread_read.thread_id = thread.thread_id AND thread_read.user_id = ?)
WHERE thread.node_id = ?
AND thread.last_post_date > ?
AND (thread_read.thread_id IS NULL OR thread.last_post_date > thread_read.thread_read_date)
' . ($ignored ? 'AND thread.user_id NOT IN (' . $db->quote($ignored) . ')' : '') . '
AND thread.discussion_state = 'visible'
AND thread.discussion_type <> 'redirect'
', array($userId, $forumId, $forumReadDate));
}
/**
* Marks the specified forum as read up to a specific time. Forum must have the
* forum_read_date key.
*
* @param array $forum Forum info
* @param integer $readDate Timestamp to mark as read until
* @param array|null $viewingUser
*
* @return boolean True if marked as read
*/
public function markForumRead(array $forum, $readDate, array $viewingUser = null)
{
$this->standardizeViewingUserReference($viewingUser);
$userId = $viewingUser['user_id'];
if (!$userId)
{
return false;
}
if (!array_key_exists('forum_read_date', $forum))
{
$forum['forum_read_date'] = $this->getUserForumReadDate($userId, $forum['node_id']);
}
if ($readDate <= $forum['forum_read_date'])
{
return false;
}
$this->_getDb()->query('
INSERT INTO xf_forum_read
(user_id, node_id, forum_read_date)
VALUES
(?, ?, ?)
ON DUPLICATE KEY UPDATE forum_read_date = VALUES(forum_read_date)
', array($userId, $forum['node_id'], $readDate));
return true;
}
/**
* Marks a forum and all sub-forums read. This can be used without a base forum
* to mark all forums as read.
*
* @param array|null $baseForum Info about base forum to mark read; may be null
* @param integer $readDate Date to set as read date
* @param array|null $viewingUser
*
* @return array A list of node IDs that were marked as read
*/
public function markForumTreeRead(array $baseForum = null, $readDate, array $viewingUser = null)
{
$this->standardizeViewingUserReference($viewingUser);
if (!$viewingUser['user_id'])
{
return array();
}
// TODO: technically, this should mark all nodes as read; need to refactor down the line
$forums = $this->getForums(array(), array(
'readUserId' => $viewingUser['user_id'],
'permissionCombinationId' => $viewingUser['permission_combination_id']
));
$forumIds = array();
foreach ($forums AS $markForum)
{
if ($baseForum && (
$markForum['lft'] < $baseForum['lft'] || $markForum['rgt'] > $baseForum['rgt'])
)
{
continue;
}
if ($this->canViewForum($markForum, $null,
XenForo_Permission::unserializePermissions($markForum['node_permission_cache'])
))
{
if ($this->markForumRead($markForum, $readDate, $viewingUser))
{
$forumIds[] = $markForum['node_id'];
}
}
}
return $forumIds;
}
/**
* Determine if the forum should be marked as read and do so if needed.
*
* @param array $forum
* @param integer $userId
* @param array|null $viewingUser
*
* @return boolean
*/
public function markForumReadIfNeeded(array $forum, array $viewingUser = null)
{
$this->standardizeViewingUserReference($viewingUser);
$userId = $viewingUser['user_id'];
if (!$userId)
{
return false;
}
if (!array_key_exists('forum_read_date', $forum))
{
$forum['forum_read_date'] = $this->getUserForumReadDate($userId, $forum['node_id']);
}
$unreadThreadCount = $this->getUnreadThreadCountInForum(
$forum['node_id'], $userId, $forum['forum_read_date'], $viewingUser['ignored']
);
if (!$unreadThreadCount)
{
return $this->markForumRead($forum, XenForo_Application::$time, $viewingUser);
}
else
{
return false;
}
}
/**
* Get the time when a user has marked the given forum as read.
*
* @param integer $userId
* @param integer $forumId
*
* @return integer|null Null if guest; timestamp otherwise
*/
public function getUserForumReadDate($userId, $forumId)
{
if (!$userId)
{
return null;
}
$readDate = $this->_getDb()->fetchOne('
SELECT forum_read_date
FROM xf_forum_read
WHERE user_id = ?
AND node_id = ?
', array($userId, $forumId));
$autoReadDate = XenForo_Application::$time - (XenForo_Application::get('options')->readMarkingDataLifetime * 86400);
return max($readDate, $autoReadDate);
}
/**
* Gets the forum counters for the specified forum.
*
* @param integer $forumId
*
* @return array Keys: discussion_count, message_count
*/
public function getForumCounters($forumId)
{
return $this->_getDb()->fetchRow('
SELECT
COUNT(*) AS discussion_count,
COUNT(*) + SUM(reply_count) AS message_count
FROM xf_thread
WHERE node_id = ?
AND discussion_state = 'visible'
AND discussion_type <> 'redirect'
', $forumId);
}
/**
* Fetch a single forum by the ID of a thread contained within it
*
* @param integer $threadId
*
* @return array
*/
public function getForumByThreadId($threadId)
{
if ($forums = $this->getForums(array('thread_id' => $threadId)))
{
return reset($forums);
}
return array();
}
public function getForumsByThreadIds(array $threadIds)
{
return $this->getForums(array('thread_ids' => $threadIds));
}
}