<?php
namespace XF\Entity;
use XF\Mvc\Entity\Structure;
/**
* COLUMNS
* @property int $node_id
* @property int $discussion_count
* @property int $message_count
* @property int $last_post_id
* @property int $last_post_date
* @property int $last_post_user_id
* @property string $last_post_username
* @property int $last_thread_id
* @property string $last_thread_title
* @property int $last_thread_prefix_id
* @property bool $moderate_threads
* @property bool $moderate_replies
* @property string $forum_type_id
* @property array $type_config_
* @property bool $allow_posting
* @property bool $count_messages
* @property bool $find_new
* @property string $allow_index
* @property array $index_criteria
* @property bool $require_prefix
* @property string $allowed_watch_notifications
* @property array $field_cache
* @property array $prefix_cache
* @property array $prompt_cache
* @property int $default_prefix_id
* @property string $default_sort_order
* @property string $default_sort_direction
* @property int $list_date_limit_days
* @property int $min_tags
*
* GETTERS
* @property \XF\Draft $draft_thread
* @property \XF\Mvc\Entity\ArrayCollection|\XF\Entity\ThreadPrefix[] $prefixes
* @property \XF\Phrase $thread_prompt
* @property \XF\ForumType\AbstractHandler $TypeHandler
* @property array $type_config
* @property string|null $node_name
* @property string|null $title
* @property string|null $description
* @property int $depth
*
* RELATIONS
* @property \XF\Mvc\Entity\AbstractCollection|\XF\Entity\ForumRead[] $Read
* @property \XF\Mvc\Entity\AbstractCollection|\XF\Entity\ForumWatch[] $Watch
* @property \XF\Mvc\Entity\AbstractCollection|\XF\Entity\Draft[] $DraftThreads
* @property \XF\Entity\Post $LastPost
* @property \XF\Entity\User $LastPostUser
* @property \XF\Entity\Thread $LastThread
* @property \XF\Entity\Node $Node
*/
class Forum extends AbstractNode
{
public function canCreateThread(&$error = null)
{
if (!$this->allow_posting)
{
$error = \XF::phraseDeferred('you_may_not_perform_this_action_because_forum_does_not_allow_posting');
return false;
}
$visitor = \XF::visitor();
return $visitor->hasNodePermission($this->node_id, 'postThread');
}
public function canCreateThreadPreReg()
{
if (\XF::visitor()->user_id || $this->canCreateThread())
{
return false;
}
return \XF::canPerformPreRegAction(
function() { return $this->canCreateThread(); }
);
}
public function canCreatePoll(&$error = null)
{
return $this->TypeHandler->isThreadTypeCreatable('poll', $this);
}
public function canViewThreadContent(): bool
{
return \XF::visitor()->hasNodePermission($this->node_id, 'viewContent');
}
public function canViewDeletedThreads()
{
return \XF::visitor()->hasNodePermission($this->node_id, 'viewDeleted');
}
public function canViewModeratedThreads()
{
return \XF::visitor()->hasNodePermission($this->node_id, 'viewModerated');
}
public function canUploadAndManageAttachments()
{
$visitor = \XF::visitor();
return ($visitor->user_id && $visitor->hasNodePermission($this->node_id, 'uploadAttachment'));
}
public function canUploadVideos()
{
$options = $this->app()->options();
if (empty($options->allowVideoUploads['enabled']))
{
return false;
}
$visitor = \XF::visitor();
return $visitor->hasNodePermission($this->node_id, 'uploadVideo');
}
public function canEditTags(Thread $thread = null, &$error = null)
{
if (!$this->app()->options()->enableTagging)
{
return false;
}
if ($thread)
{
if (!$thread->discussion_open && !$thread->canLockUnlock())
{
$error = \XF::phraseDeferred('you_may_not_perform_this_action_because_discussion_is_closed');
return false;
}
}
$visitor = \XF::visitor();
// if no thread, assume the thread will be owned by this person
if (!$thread || $thread->user_id == $visitor->user_id)
{
if ($visitor->hasNodePermission($this->node_id, 'tagOwnThread'))
{
return true;
}
}
if (
$visitor->hasNodePermission($this->node_id, 'tagAnyThread')
|| $visitor->hasNodePermission($this->node_id, 'manageAnyTag')
)
{
return true;
}
return false;
}
public function canWatch(&$error = null)
{
return \XF::visitor()->user_id ? true : false;
}
public function isUnread()
{
if (!$this->discussion_count)
{
return false;
}
$cutOff = \XF::$time - $this->app()->options()->readMarkingDataLifetime * 86400;
if ($this->last_post_date < $cutOff)
{
return false;
}
$visitor = \XF::visitor();
if ($visitor->user_id)
{
$read = $this->Read[$visitor->user_id];
return (!$read || $read->forum_read_date < $this->last_post_date);
}
else
{
return true;
}
}
public function isSearchEngineIndexable(): bool
{
if ($this->allow_index == 'deny')
{
return false;
}
return true;
}
/**
* @param bool $current If true, gets the current value; else gets the previous value
* @param bool $fallback If true, falls back to the basic forum type if the type is invalid
*
* @return \XF\ForumType\AbstractHandler
*/
public function getTypeHandler(bool $current = true, bool $fallback = true)
{
$forumTypeId = $current ? $this->forum_type_id : $this->getExistingValue('forum_type_id');
$forumType = $this->app()->forumType($forumTypeId, false);
if (!$forumType && $fallback)
{
$forumType = $this->app()->forumType('discussion');
}
return $forumType;
}
/**
* @return array
*/
public function getTypeConfig()
{
$config = $this->TypeHandler->getDefaultTypeConfig();
return array_replace($config, $this->type_config_);
}
/**
* @return array
*/
public function getCreatableThreadTypes()
{
return $this->TypeHandler->getCreatableThreadTypes($this);
}
/**
* @param string $type
*
* @return bool
*/
public function isThreadTypeCreatable($type)
{
return $this->TypeHandler->isThreadTypeCreatable($type, $this);
}
/**
* @return \XF\Draft
*/
public function getDraftThread()
{
return \XF\Draft::createFromEntity($this, 'DraftThreads');
}
public function getUsablePrefixes(ThreadPrefix $forcePrefix = null)
{
$prefixes = $this->prefixes;
$prefixes = $prefixes->filter(function ($prefix) use ($forcePrefix)
{
if ($forcePrefix && $forcePrefix->prefix_id == $prefix->prefix_id)
{
return true;
}
return $this->isPrefixUsable($prefix);
});
return $prefixes->groupBy('prefix_group_id');
}
public function getPrefixesGrouped()
{
return $this->prefixes->groupBy('prefix_group_id');
}
/**
* @return \XF\Mvc\Entity\ArrayCollection|\XF\Entity\ThreadPrefix[]
*/
public function getPrefixes()
{
if (!$this->prefix_cache)
{
return $this->_em->getEmptyCollection();
}
$prefixes = $this->finder('XF:ThreadPrefix')
->where('prefix_id', $this->prefix_cache)
->order('materialized_order')
->fetch();
return $prefixes;
}
public function isPrefixUsable($prefix, User $user = null)
{
if (!$this->isPrefixValid($prefix))
{
return false;
}
if (!($prefix instanceof ThreadPrefix))
{
$prefix = $this->em()->find('XF:ThreadPrefix', $prefix);
if (!$prefix)
{
return false;
}
}
return $prefix->isUsableByUser($user);
}
public function isPrefixValid($prefix)
{
if ($prefix instanceof ThreadPrefix)
{
$prefix = $prefix->prefix_id;
}
return (!$prefix || isset($this->prefix_cache[$prefix]));
}
public function getNodeListExtras()
{
if (\XF::visitor()->hasNodePermission($this->node_id, 'viewOthers'))
{
$output = [
'discussion_count' => $this->discussion_count,
'message_count' => $this->message_count,
'hasNew' => $this->isUnread()
];
if ($this->last_post_date)
{
$output['last_post_id'] = $this->last_post_id;
$output['last_post_date'] = $this->last_post_date;
$output['last_post_user_id'] = $this->last_post_user_id;
$output['last_post_username'] = $this->last_post_username;
$output['last_thread_id'] = $this->last_thread_id;
$output['last_thread_title'] = $this->app()->stringFormatter()->censorText($this->last_thread_title);
$output['last_thread_prefix_id'] = $this->last_thread_prefix_id;
$output['LastPostUser'] = $this->LastPostUser;
$output['LastThread'] = $this->LastThread;
}
return $output;
}
else
{
return ['privateInfo' => true];
}
}
public function getNodeTemplateRenderer($depth)
{
return [
'template' => 'node_list_forum',
'macro' => $depth <= 2 ? 'depth' . $depth : 'depthN'
];
}
public function getNewContentState(Thread $thread = null)
{
$visitor = \XF::visitor();
if ($visitor->user_id && $visitor->hasNodePermission($this->node_id, 'approveUnapprove'))
{
return 'visible';
}
if (!$visitor->hasPermission('general', 'submitWithoutApproval'))
{
return 'moderated';
}
if ($thread)
{
return $this->moderate_replies ? 'moderated' : 'visible';
}
else
{
return $this->moderate_threads ? 'moderated' : 'visible';
}
}
public function getNewThread()
{
$thread = $this->_em->create('XF:Thread');
$thread->node_id = $this->node_id;
return $thread;
}
public function threadAdded(Thread $thread)
{
if ($thread->discussion_type == 'redirect')
{
return;
}
$this->discussion_count++;
$this->message_count += 1 + $thread->reply_count;
if ($thread->last_post_date >= $this->last_post_date)
{
$this->last_post_date = $thread->last_post_date;
$this->last_post_id = $thread->last_post_id;
$this->last_post_user_id = $thread->last_post_user_id;
$this->last_post_username = $thread->last_post_username;
$this->last_thread_id = $thread->thread_id;
$this->last_thread_title = $thread->title;
$this->last_thread_prefix_id = $thread->prefix_id;
}
}
public function threadDataChanged(Thread $thread)
{
$isRedirect = $thread->discussion_type == 'redirect';
$wasRedirect = $thread->getExistingValue('discussion_type') == 'redirect';
if ($isRedirect && !$wasRedirect)
{
// this is like the thread being deleted for counter purposes
$this->threadRemoved($thread);
}
else if (!$isRedirect && $wasRedirect)
{
// like being added
$this->threadAdded($thread);
}
else if ($isRedirect)
{
return;
}
$this->message_count += $thread->reply_count - $thread->getExistingValue('reply_count');
if ($thread->last_post_date >= $this->last_post_date)
{
$this->last_post_date = $thread->last_post_date;
$this->last_post_id = $thread->last_post_id;
$this->last_post_user_id = $thread->last_post_user_id;
$this->last_post_username = $thread->last_post_username;
$this->last_thread_id = $thread->thread_id;
$this->last_thread_title = $thread->title;
$this->last_thread_prefix_id = $thread->prefix_id;
}
else if ($thread->getExistingValue('last_post_id') == $this->last_post_id)
{
$this->rebuildLastPost();
}
}
public function threadRemoved(Thread $thread)
{
if ($thread->discussion_type == 'redirect')
{
// if this was changed, it used to count so we need to continue
if (!$thread->isChanged('discussion_type'))
{
return;
}
}
$this->discussion_count--;
$this->message_count -= 1 + $thread->reply_count;
if ($thread->last_post_id == $this->last_post_id)
{
$this->rebuildLastPost();
}
}
public function rebuildCounters()
{
$counters = $this->db()->fetchRow("
SELECT COUNT(*) AS discussion_count,
COUNT(*) + COALESCE(SUM(reply_count), 0) AS message_count
FROM xf_thread
WHERE node_id = ?
AND discussion_state = 'visible'
AND discussion_type <> 'redirect'
", $this->node_id);
$this->discussion_count = $counters['discussion_count'];
$this->message_count = $counters['message_count'];
$this->rebuildLastPost();
}
public function rebuildLastPost()
{
$thread = $this->db()->fetchRow("
SELECT *
FROM xf_thread
WHERE node_id = ?
AND discussion_state = 'visible'
AND discussion_type <> 'redirect'
ORDER BY last_post_date DESC
LIMIT 1
", $this->node_id);
if ($thread)
{
$this->last_post_id = $thread['last_post_id'];
$this->last_post_date = $thread['last_post_date'];
$this->last_post_user_id = $thread['last_post_user_id'];
$this->last_post_username = $thread['last_post_username'];
$this->last_thread_id = $thread['thread_id'];
$this->last_thread_title = $thread['title'];
$this->last_thread_prefix_id = $thread['prefix_id'];
}
else
{
$this->last_post_id = 0;
$this->last_post_date = 0;
$this->last_post_user_id = 0;
$this->last_post_username = '';
$this->last_thread_id = 0;
$this->last_thread_title = '';
$this->last_thread_prefix_id = 0;
}
}
protected function _preSave()
{
if ($this->isChanged('default_sort_order'))
{
$sortOrders = $this->TypeHandler->getThreadListSortOptions($this, true);
if (!isset($sortOrders[$this->default_sort_order]))
{
$this->error(\XF::phrase('please_select_valid_default_sort_order'), 'default_sort_order');
}
}
else if ($this->isChanged('forum_type_id'))
{
$sortOrders = $this->TypeHandler->getThreadListSortOptions($this, true);
if (!isset($sortOrders[$this->default_sort_order]))
{
// we're changing the type but the selected order is no longer valid, so switch to the first available
$this->default_sort_order = key($sortOrders);
}
}
}
protected function _postDelete()
{
$this->db()->delete('xf_forum_prefix', 'node_id = ?', $this->node_id);
$this->db()->delete('xf_forum_watch', 'node_id = ?', $this->node_id);
if ($this->getOption('delete_threads'))
{
$this->app()->jobManager()->enqueueUnique('forumDelete' . $this->node_id, 'XF:ForumDelete', [
'node_id' => $this->node_id
]);
}
if ($this->node_id == \XF::options()->reportIntoForumId)
{
$this->repository('XF:Option')->updateOption('reportIntoForumId', 0);
}
}
public function getNodeTypeApiData($verbosity = self::VERBOSITY_NORMAL, array $options = [])
{
$result = parent::getNodeTypeApiData();
if (\XF::visitor()->hasNodePermission($this->node_id, 'viewOthers'))
{
$result->includeExtra([
'is_unread' => $this->isUnread(),
'discussion_count' => $this->discussion_count,
'message_count' => $this->message_count,
'last_post_id' => $this->last_post_id,
'last_post_date' => $this->last_post_date,
'last_post_username' => $this->last_post_username,
'last_thread_id' => $this->last_thread_id,
'last_thread_title' => $this->app()->stringFormatter()->censorText($this->last_thread_title),
'last_thread_prefix_id' => $this->last_thread_prefix_id,
]);
}
$this->TypeHandler->addTypeConfigToApiResult($this, $result, $verbosity, $options);
if ($verbosity > self::VERBOSITY_NORMAL)
{
$result->prefixes = $this->prefixes->toApiResults();
$fields = [];
if ($this->field_cache)
{
$fieldEntities = $this->repository('XF:ThreadField')->findFieldsForList()
->whereIds($this->field_cache)
->fetch();
$fields = $fieldEntities->toApiResults();
}
$result->custom_fields = $fields;
}
$result->can_create_thread = $this->canCreateThread();
$result->can_upload_attachment = $this->canUploadAndManageAttachments();
return $result;
}
public static function getStructure(Structure $structure)
{
$structure->table = 'xf_forum';
$structure->shortName = 'XF:Forum';
$structure->contentType = 'forum';
$structure->primaryKey = 'node_id';
$structure->columns = [
'node_id' => ['type' => self::UINT, 'required' => true],
'discussion_count' => ['type' => self::UINT, 'forced' => true, 'default' => 0],
'message_count' => ['type' => self::UINT, 'forced' => true, 'default' => 0],
'last_post_id' => ['type' => self::UINT, 'default' => 0],
'last_post_date' => ['type' => self::UINT, 'default' => 0],
'last_post_user_id' => ['type' => self::UINT, 'default' => 0],
'last_post_username' => ['type' => self::STR, 'maxLength' => 50, 'default' => ''],
'last_thread_id' => ['type' => self::UINT, 'default' => 0],
'last_thread_title' => ['type' => self::STR, 'maxLength' => 150, 'default' => ''],
'last_thread_prefix_id' => ['type' => self::UINT, 'default' => 0],
'moderate_threads' => ['type' => self::BOOL, 'default' => false],
'moderate_replies' => ['type' => self::BOOL, 'default' => false],
'forum_type_id' => ['type' => self::STR, 'default' => 'discussion', 'api' => true],
'type_config' => ['type' => self::JSON_ARRAY, 'default' => []],
'allow_posting' => ['type' => self::BOOL, 'default' => true, 'api' => true],
'count_messages' => ['type' => self::BOOL, 'default' => true],
'find_new' => ['type' => self::BOOL, 'default' => true],
'allow_index' => ['type' => self::STR, 'default' => 'allow',
'allowedValues' => ['allow', 'deny', 'criteria']
],
'index_criteria' => ['type' => self::JSON_ARRAY, 'default' => []],
'require_prefix' => ['type' => self::BOOL, 'default' => false, 'api' => true],
'allowed_watch_notifications' => ['type' => self::STR, 'default' => 'all',
'allowedValues' => ['all', 'thread', 'none']
],
'field_cache' => ['type' => self::JSON_ARRAY, 'default' => []],
'prefix_cache' => ['type' => self::JSON_ARRAY, 'default' => []],
'prompt_cache' => ['type' => self::JSON_ARRAY, 'default' => []],
'default_prefix_id' => ['type' => self::UINT, 'default' => 0],
'default_sort_order' => ['type' => self::STR, 'default' => 'last_post_date'],
'default_sort_direction' => ['type' => self::STR, 'default' => 'desc',
'allowedValues' => ['asc', 'desc']
],
'list_date_limit_days' => ['type' => self::UINT, 'default' => 0, 'max' => 3650],
'min_tags' => ['type' => self::UINT, 'default' => 0, 'max' => 100, 'api' => true],
];
$structure->getters = [
'draft_thread' => true,
'prefixes' => true,
'thread_prompt' => true,
'TypeHandler' => [
'cache' => true,
'getter' => 'getTypeHandler',
'invalidate' => ['forum_type_id']
],
'type_config' => [
'cache' => true,
'getter' => 'getTypeConfig',
'invalidate' => ['forum_type_id']
],
];
$structure->relations = [
'Read' => [
'entity' => 'XF:ForumRead',
'type' => self::TO_MANY,
'conditions' => 'node_id',
'key' => 'user_id'
],
'Watch' => [
'entity' => 'XF:ForumWatch',
'type' => self::TO_MANY,
'conditions' => 'node_id',
'key' => 'user_id'
],
'DraftThreads' => [
'entity' => 'XF:Draft',
'type' => self::TO_MANY,
'conditions' => [
['draft_key', '=', 'forum-', '$node_id']
],
'key' => 'user_id'
],
'LastPost' => [
'entity' => 'XF:Post',
'type' => self::TO_ONE,
'conditions' => [['post_id', '=', '$last_post_id']],
'primary' => true
],
'LastPostUser' => [
'entity' => 'XF:User',
'type' => self::TO_ONE,
'conditions' => [['user_id', '=', '$last_post_user_id']],
'primary' => true
],
'LastThread' => [
'entity' => 'XF:Thread',
'type' => self::TO_ONE,
'conditions' => [['thread_id', '=', '$last_thread_id']],
'primary' => true
]
];
$structure->options = [
'delete_threads' => true
];
$structure->withAliases = [
'api' => [
function()
{
$userId = \XF::visitor()->user_id;
if ($userId)
{
return [
'Read|' . $userId
];
}
}
]
];
static::addDefaultNodeElements($structure);
return $structure;
}
public static function getListedWith()
{
$visitor = \XF::visitor();
$with = ['LastPostUser', 'LastThread'];
if ($visitor->user_id)
{
$with[] = "Read|{$visitor->user_id}";
$with[] = "LastThread.Read|{$visitor->user_id}";
}
return $with;
}
/**
* @return \XF\Phrase
*/
public function getThreadPrompt()
{
static $phraseName; // always return the same phrase for the same forum instance
if (!$phraseName)
{
if ($this->prompt_cache)
{
$phraseName = 'thread_prompt.' . array_rand($this->prompt_cache);
}
else
{
$phraseName = 'thread_prompt.default';
}
}
return \XF::phrase($phraseName);
}
}