Chris@0: 'entity.manager']; Chris@0: Chris@0: /** Chris@0: * Forum sort order, newest first. Chris@0: */ Chris@0: const NEWEST_FIRST = 1; Chris@0: Chris@0: /** Chris@0: * Forum sort order, oldest first. Chris@0: */ Chris@0: const OLDEST_FIRST = 2; Chris@0: Chris@0: /** Chris@0: * Forum sort order, posts with most comments first. Chris@0: */ Chris@0: const MOST_POPULAR_FIRST = 3; Chris@0: Chris@0: /** Chris@0: * Forum sort order, posts with the least comments first. Chris@0: */ Chris@0: const LEAST_POPULAR_FIRST = 4; Chris@0: Chris@0: /** Chris@0: * Forum settings config object. Chris@0: * Chris@0: * @var \Drupal\Core\Config\ConfigFactoryInterface Chris@0: */ Chris@0: protected $configFactory; Chris@0: Chris@0: /** Chris@18: * Entity field manager. Chris@0: * Chris@18: * @var \Drupal\Core\Entity\EntityFieldManagerInterface Chris@0: */ Chris@18: protected $entityFieldManager; Chris@18: Chris@18: /** Chris@18: * Entity type manager. Chris@18: * Chris@18: * @var \Drupal\Core\Entity\EntityTypeManagerInterface Chris@18: */ Chris@18: protected $entityTypeManager; Chris@0: Chris@0: /** Chris@0: * Database connection Chris@0: * Chris@0: * @var \Drupal\Core\Database\Connection Chris@0: */ Chris@0: protected $connection; Chris@0: Chris@0: /** Chris@0: * The comment manager service. Chris@0: * Chris@0: * @var \Drupal\comment\CommentManagerInterface Chris@0: */ Chris@0: protected $commentManager; Chris@0: Chris@0: /** Chris@0: * Array of last post information keyed by forum (term) id. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: protected $lastPostData = []; Chris@0: Chris@0: /** Chris@0: * Array of forum statistics keyed by forum (term) id. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: protected $forumStatistics = []; Chris@0: Chris@0: /** Chris@0: * Array of forum children keyed by parent forum (term) id. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: protected $forumChildren = []; Chris@0: Chris@0: /** Chris@0: * Array of history keyed by nid. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: protected $history = []; Chris@0: Chris@0: /** Chris@0: * Cached forum index. Chris@0: * Chris@0: * @var \Drupal\taxonomy\TermInterface Chris@0: */ Chris@0: protected $index; Chris@0: Chris@0: /** Chris@0: * Constructs the forum manager service. Chris@0: * Chris@0: * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory Chris@0: * The config factory service. Chris@18: * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager Chris@18: * The entity type manager. Chris@0: * @param \Drupal\Core\Database\Connection $connection Chris@0: * The current database connection. Chris@0: * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation Chris@0: * The translation manager service. Chris@0: * @param \Drupal\comment\CommentManagerInterface $comment_manager Chris@0: * The comment manager service. Chris@18: * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager Chris@18: * The entity field manager. Chris@0: */ Chris@18: public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, Connection $connection, TranslationInterface $string_translation, CommentManagerInterface $comment_manager, EntityFieldManagerInterface $entity_field_manager = NULL) { Chris@0: $this->configFactory = $config_factory; Chris@18: $this->entityTypeManager = $entity_type_manager; Chris@0: $this->connection = $connection; Chris@0: $this->stringTranslation = $string_translation; Chris@0: $this->commentManager = $comment_manager; Chris@18: if (!$entity_field_manager) { Chris@18: @trigger_error('The entity_field.manager service must be passed to ForumManager::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); Chris@18: $entity_field_manager = \Drupal::service('entity_field.manager'); Chris@18: } Chris@18: $this->entityFieldManager = $entity_field_manager; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getTopics($tid, AccountInterface $account) { Chris@0: $config = $this->configFactory->get('forum.settings'); Chris@0: $forum_per_page = $config->get('topics.page_limit'); Chris@0: $sortby = $config->get('topics.order'); Chris@0: Chris@0: $header = [ Chris@0: ['data' => $this->t('Topic'), 'field' => 'f.title'], Chris@0: ['data' => $this->t('Replies'), 'field' => 'f.comment_count'], Chris@0: ['data' => $this->t('Last reply'), 'field' => 'f.last_comment_timestamp'], Chris@0: ]; Chris@0: Chris@0: $order = $this->getTopicOrder($sortby); Chris@0: for ($i = 0; $i < count($header); $i++) { Chris@0: if ($header[$i]['field'] == $order['field']) { Chris@0: $header[$i]['sort'] = $order['sort']; Chris@0: } Chris@0: } Chris@0: Chris@0: $query = $this->connection->select('forum_index', 'f') Chris@0: ->extend('Drupal\Core\Database\Query\PagerSelectExtender') Chris@0: ->extend('Drupal\Core\Database\Query\TableSortExtender'); Chris@0: $query->fields('f'); Chris@0: $query Chris@0: ->condition('f.tid', $tid) Chris@0: ->addTag('node_access') Chris@0: ->addMetaData('base_table', 'forum_index') Chris@0: ->orderBy('f.sticky', 'DESC') Chris@0: ->orderByHeader($header) Chris@0: ->limit($forum_per_page); Chris@0: Chris@0: $count_query = $this->connection->select('forum_index', 'f'); Chris@0: $count_query->condition('f.tid', $tid); Chris@0: $count_query->addExpression('COUNT(*)'); Chris@0: $count_query->addTag('node_access'); Chris@0: $count_query->addMetaData('base_table', 'forum_index'); Chris@0: Chris@0: $query->setCountQuery($count_query); Chris@0: $result = $query->execute(); Chris@0: $nids = []; Chris@0: foreach ($result as $record) { Chris@0: $nids[] = $record->nid; Chris@0: } Chris@0: if ($nids) { Chris@18: $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids); Chris@0: Chris@0: $query = $this->connection->select('node_field_data', 'n') Chris@0: ->extend('Drupal\Core\Database\Query\TableSortExtender'); Chris@0: $query->fields('n', ['nid']); Chris@0: Chris@0: $query->join('comment_entity_statistics', 'ces', "n.nid = ces.entity_id AND ces.field_name = 'comment_forum' AND ces.entity_type = 'node'"); Chris@0: $query->fields('ces', [ Chris@0: 'cid', Chris@0: 'last_comment_uid', Chris@0: 'last_comment_timestamp', Chris@17: 'comment_count', Chris@0: ]); Chris@0: Chris@0: $query->join('forum_index', 'f', 'f.nid = n.nid'); Chris@0: $query->addField('f', 'tid', 'forum_tid'); Chris@0: Chris@0: $query->join('users_field_data', 'u', 'n.uid = u.uid AND u.default_langcode = 1'); Chris@0: $query->addField('u', 'name'); Chris@0: Chris@0: $query->join('users_field_data', 'u2', 'ces.last_comment_uid = u2.uid AND u.default_langcode = 1'); Chris@0: Chris@0: $query->addExpression('CASE ces.last_comment_uid WHEN 0 THEN ces.last_comment_name ELSE u2.name END', 'last_comment_name'); Chris@0: Chris@0: $query Chris@0: ->orderBy('f.sticky', 'DESC') Chris@0: ->orderByHeader($header) Chris@0: ->condition('n.nid', $nids, 'IN') Chris@0: // @todo This should be actually filtering on the desired node language Chris@0: // and just fall back to the default language. Chris@0: ->condition('n.default_langcode', 1); Chris@0: Chris@0: $result = []; Chris@0: foreach ($query->execute() as $row) { Chris@0: $topic = $nodes[$row->nid]; Chris@0: $topic->comment_mode = $topic->comment_forum->status; Chris@0: Chris@0: foreach ($row as $key => $value) { Chris@0: $topic->{$key} = $value; Chris@0: } Chris@0: $result[] = $topic; Chris@0: } Chris@0: } Chris@0: else { Chris@0: $result = []; Chris@0: } Chris@0: Chris@0: $topics = []; Chris@0: $first_new_found = FALSE; Chris@0: foreach ($result as $topic) { Chris@0: if ($account->isAuthenticated()) { Chris@0: // A forum is new if the topic is new, or if there are new comments since Chris@0: // the user's last visit. Chris@0: if ($topic->forum_tid != $tid) { Chris@0: $topic->new = 0; Chris@0: } Chris@0: else { Chris@0: $history = $this->lastVisit($topic->id(), $account); Chris@0: $topic->new_replies = $this->commentManager->getCountNewComments($topic, 'comment_forum', $history); Chris@0: $topic->new = $topic->new_replies || ($topic->last_comment_timestamp > $history); Chris@0: } Chris@0: } Chris@0: else { Chris@0: // Do not track "new replies" status for topics if the user is anonymous. Chris@0: $topic->new_replies = 0; Chris@0: $topic->new = 0; Chris@0: } Chris@0: Chris@0: // Make sure only one topic is indicated as the first new topic. Chris@0: $topic->first_new = FALSE; Chris@0: if ($topic->new != 0 && !$first_new_found) { Chris@0: $topic->first_new = TRUE; Chris@0: $first_new_found = TRUE; Chris@0: } Chris@0: Chris@0: if ($topic->comment_count > 0) { Chris@0: $last_reply = new \stdClass(); Chris@0: $last_reply->created = $topic->last_comment_timestamp; Chris@0: $last_reply->name = $topic->last_comment_name; Chris@0: $last_reply->uid = $topic->last_comment_uid; Chris@0: $topic->last_reply = $last_reply; Chris@0: } Chris@0: $topics[$topic->id()] = $topic; Chris@0: } Chris@0: Chris@0: return ['topics' => $topics, 'header' => $header]; Chris@0: Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets topic sorting information based on an integer code. Chris@0: * Chris@0: * @param int $sortby Chris@0: * One of the following integers indicating the sort criteria: Chris@0: * - ForumManager::NEWEST_FIRST: Date - newest first. Chris@0: * - ForumManager::OLDEST_FIRST: Date - oldest first. Chris@0: * - ForumManager::MOST_POPULAR_FIRST: Posts with the most comments first. Chris@0: * - ForumManager::LEAST_POPULAR_FIRST: Posts with the least comments first. Chris@0: * Chris@0: * @return array Chris@0: * An array with the following values: Chris@0: * - field: A field for an SQL query. Chris@0: * - sort: 'asc' or 'desc'. Chris@0: */ Chris@0: protected function getTopicOrder($sortby) { Chris@0: switch ($sortby) { Chris@0: case static::NEWEST_FIRST: Chris@0: return ['field' => 'f.last_comment_timestamp', 'sort' => 'desc']; Chris@0: Chris@0: case static::OLDEST_FIRST: Chris@0: return ['field' => 'f.last_comment_timestamp', 'sort' => 'asc']; Chris@0: Chris@0: case static::MOST_POPULAR_FIRST: Chris@0: return ['field' => 'f.comment_count', 'sort' => 'desc']; Chris@0: Chris@0: case static::LEAST_POPULAR_FIRST: Chris@0: return ['field' => 'f.comment_count', 'sort' => 'asc']; Chris@0: Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the last time the user viewed a node. Chris@0: * Chris@0: * @param int $nid Chris@0: * The node ID. Chris@0: * @param \Drupal\Core\Session\AccountInterface $account Chris@0: * Account to fetch last time for. Chris@0: * Chris@0: * @return int Chris@0: * The timestamp when the user last viewed this node, if the user has Chris@0: * previously viewed the node; otherwise HISTORY_READ_LIMIT. Chris@0: */ Chris@0: protected function lastVisit($nid, AccountInterface $account) { Chris@0: if (empty($this->history[$nid])) { Chris@0: $result = $this->connection->select('history', 'h') Chris@0: ->fields('h', ['nid', 'timestamp']) Chris@0: ->condition('uid', $account->id()) Chris@0: ->execute(); Chris@0: foreach ($result as $t) { Chris@0: $this->history[$t->nid] = $t->timestamp > HISTORY_READ_LIMIT ? $t->timestamp : HISTORY_READ_LIMIT; Chris@0: } Chris@0: } Chris@0: return isset($this->history[$nid]) ? $this->history[$nid] : HISTORY_READ_LIMIT; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Provides the last post information for the given forum tid. Chris@0: * Chris@0: * @param int $tid Chris@0: * The forum tid. Chris@0: * Chris@0: * @return \stdClass Chris@0: * The last post for the given forum. Chris@0: */ Chris@0: protected function getLastPost($tid) { Chris@0: if (!empty($this->lastPostData[$tid])) { Chris@0: return $this->lastPostData[$tid]; Chris@0: } Chris@0: // Query "Last Post" information for this forum. Chris@0: $query = $this->connection->select('node_field_data', 'n'); Chris@0: $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', [':tid' => $tid]); Chris@0: $query->join('comment_entity_statistics', 'ces', "n.nid = ces.entity_id AND ces.field_name = 'comment_forum' AND ces.entity_type = 'node'"); Chris@0: $query->join('users_field_data', 'u', 'ces.last_comment_uid = u.uid AND u.default_langcode = 1'); Chris@0: $query->addExpression('CASE ces.last_comment_uid WHEN 0 THEN ces.last_comment_name ELSE u.name END', 'last_comment_name'); Chris@0: Chris@0: $topic = $query Chris@0: ->fields('ces', ['last_comment_timestamp', 'last_comment_uid']) Chris@0: ->condition('n.status', 1) Chris@0: ->orderBy('last_comment_timestamp', 'DESC') Chris@0: ->range(0, 1) Chris@0: ->addTag('node_access') Chris@0: ->execute() Chris@0: ->fetchObject(); Chris@0: Chris@0: // Build the last post information. Chris@0: $last_post = new \stdClass(); Chris@0: if (!empty($topic->last_comment_timestamp)) { Chris@0: $last_post->created = $topic->last_comment_timestamp; Chris@0: $last_post->name = $topic->last_comment_name; Chris@0: $last_post->uid = $topic->last_comment_uid; Chris@0: } Chris@0: Chris@0: $this->lastPostData[$tid] = $last_post; Chris@0: return $last_post; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Provides statistics for a forum. Chris@0: * Chris@0: * @param int $tid Chris@0: * The forum tid. Chris@0: * Chris@0: * @return \stdClass|null Chris@0: * Statistics for the given forum if statistics exist, else NULL. Chris@0: */ Chris@0: protected function getForumStatistics($tid) { Chris@0: if (empty($this->forumStatistics)) { Chris@0: // Prime the statistics. Chris@0: $query = $this->connection->select('node_field_data', 'n'); Chris@0: $query->join('comment_entity_statistics', 'ces', "n.nid = ces.entity_id AND ces.field_name = 'comment_forum' AND ces.entity_type = 'node'"); Chris@0: $query->join('forum', 'f', 'n.vid = f.vid'); Chris@0: $query->addExpression('COUNT(n.nid)', 'topic_count'); Chris@0: $query->addExpression('SUM(ces.comment_count)', 'comment_count'); Chris@0: $this->forumStatistics = $query Chris@0: ->fields('f', ['tid']) Chris@0: ->condition('n.status', 1) Chris@0: ->condition('n.default_langcode', 1) Chris@0: ->groupBy('tid') Chris@0: ->addTag('node_access') Chris@0: ->execute() Chris@0: ->fetchAllAssoc('tid'); Chris@0: } Chris@0: Chris@0: if (!empty($this->forumStatistics[$tid])) { Chris@0: return $this->forumStatistics[$tid]; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getChildren($vid, $tid) { Chris@0: if (!empty($this->forumChildren[$tid])) { Chris@0: return $this->forumChildren[$tid]; Chris@0: } Chris@0: $forums = []; Chris@18: $_forums = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vid, $tid, NULL, TRUE); Chris@0: foreach ($_forums as $forum) { Chris@0: // Merge in the topic and post counters. Chris@0: if (($count = $this->getForumStatistics($forum->id()))) { Chris@0: $forum->num_topics = $count->topic_count; Chris@0: $forum->num_posts = $count->topic_count + $count->comment_count; Chris@0: } Chris@0: else { Chris@0: $forum->num_topics = 0; Chris@0: $forum->num_posts = 0; Chris@0: } Chris@0: Chris@0: // Merge in last post details. Chris@0: $forum->last_post = $this->getLastPost($forum->id()); Chris@0: $forums[$forum->id()] = $forum; Chris@0: } Chris@0: Chris@0: $this->forumChildren[$tid] = $forums; Chris@0: return $forums; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getIndex() { Chris@0: if ($this->index) { Chris@0: return $this->index; Chris@0: } Chris@0: Chris@0: $vid = $this->configFactory->get('forum.settings')->get('vocabulary'); Chris@18: $index = $this->entityTypeManager->getStorage('taxonomy_term')->create([ Chris@0: 'tid' => 0, Chris@0: 'container' => 1, Chris@0: 'parents' => [], Chris@0: 'isIndex' => TRUE, Chris@17: 'vid' => $vid, Chris@0: ]); Chris@0: Chris@0: // Load the tree below. Chris@0: $index->forums = $this->getChildren($vid, 0); Chris@0: $this->index = $index; Chris@0: return $index; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function resetCache() { Chris@0: // Reset the index. Chris@0: $this->index = NULL; Chris@0: // Reset history. Chris@0: $this->history = []; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getParents($tid) { Chris@18: return $this->entityTypeManager->getStorage('taxonomy_term')->loadAllParents($tid); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function checkNodeType(NodeInterface $node) { Chris@0: // Fetch information about the forum field. Chris@18: $field_definitions = $this->entityFieldManager->getFieldDefinitions('node', $node->bundle()); Chris@0: return !empty($field_definitions['taxonomy_forums']); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function unreadTopics($term, $uid) { Chris@0: $query = $this->connection->select('node_field_data', 'n'); Chris@0: $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', [':tid' => $term]); Chris@0: $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', [':uid' => $uid]); Chris@0: $query->addExpression('COUNT(n.nid)', 'count'); Chris@0: return $query Chris@0: ->condition('status', 1) Chris@0: // @todo This should be actually filtering on the desired node status Chris@0: // field language and just fall back to the default language. Chris@0: ->condition('n.default_langcode', 1) Chris@0: ->condition('n.created', HISTORY_READ_LIMIT, '>') Chris@0: ->isNull('h.nid') Chris@0: ->addTag('node_access') Chris@0: ->execute() Chris@0: ->fetchField(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function __sleep() { Chris@0: $vars = $this->defaultSleep(); Chris@0: // Do not serialize static cache. Chris@0: unset($vars['history'], $vars['index'], $vars['lastPostData'], $vars['forumChildren'], $vars['forumStatistics']); Chris@0: return $vars; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function __wakeup() { Chris@0: $this->defaultWakeup(); Chris@0: // Initialize static cache. Chris@0: $this->history = []; Chris@0: $this->lastPostData = []; Chris@0: $this->forumChildren = []; Chris@0: $this->forumStatistics = []; Chris@0: $this->index = NULL; Chris@0: } Chris@0: Chris@0: }