Updated to Drupal 8.6.4, which is PHP 7.3 friendly. Also updated HTMLaw library....
[yaffs-website] / web / core / modules / forum / src / ForumManager.php
1 <?php
2
3 namespace Drupal\forum;
4
5 use Drupal\Core\Config\ConfigFactoryInterface;
6 use Drupal\Core\Database\Connection;
7 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
8 use Drupal\Core\Entity\EntityManagerInterface;
9 use Drupal\Core\Session\AccountInterface;
10 use Drupal\Core\StringTranslation\TranslationInterface;
11 use Drupal\Core\StringTranslation\StringTranslationTrait;
12 use Drupal\comment\CommentManagerInterface;
13 use Drupal\node\NodeInterface;
14
15 /**
16  * Provides forum manager service.
17  */
18 class ForumManager implements ForumManagerInterface {
19   use StringTranslationTrait;
20   use DependencySerializationTrait {
21     __wakeup as defaultWakeup;
22     __sleep as defaultSleep;
23   }
24
25   /**
26    * Forum sort order, newest first.
27    */
28   const NEWEST_FIRST = 1;
29
30   /**
31    * Forum sort order, oldest first.
32    */
33   const OLDEST_FIRST = 2;
34
35   /**
36    * Forum sort order, posts with most comments first.
37    */
38   const MOST_POPULAR_FIRST = 3;
39
40   /**
41    * Forum sort order, posts with the least comments first.
42    */
43   const LEAST_POPULAR_FIRST = 4;
44
45   /**
46    * Forum settings config object.
47    *
48    * @var \Drupal\Core\Config\ConfigFactoryInterface
49    */
50   protected $configFactory;
51
52   /**
53    * Entity manager service
54    *
55    * @var \Drupal\Core\Entity\EntityManagerInterface
56    */
57   protected $entityManager;
58
59   /**
60    * Database connection
61    *
62    * @var \Drupal\Core\Database\Connection
63    */
64   protected $connection;
65
66   /**
67    * The comment manager service.
68    *
69    * @var \Drupal\comment\CommentManagerInterface
70    */
71   protected $commentManager;
72
73   /**
74    * Array of last post information keyed by forum (term) id.
75    *
76    * @var array
77    */
78   protected $lastPostData = [];
79
80   /**
81    * Array of forum statistics keyed by forum (term) id.
82    *
83    * @var array
84    */
85   protected $forumStatistics = [];
86
87   /**
88    * Array of forum children keyed by parent forum (term) id.
89    *
90    * @var array
91    */
92   protected $forumChildren = [];
93
94   /**
95    * Array of history keyed by nid.
96    *
97    * @var array
98    */
99   protected $history = [];
100
101   /**
102    * Cached forum index.
103    *
104    * @var \Drupal\taxonomy\TermInterface
105    */
106   protected $index;
107
108   /**
109    * Constructs the forum manager service.
110    *
111    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
112    *   The config factory service.
113    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
114    *   The entity manager service.
115    * @param \Drupal\Core\Database\Connection $connection
116    *   The current database connection.
117    * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
118    *   The translation manager service.
119    * @param \Drupal\comment\CommentManagerInterface $comment_manager
120    *   The comment manager service.
121    */
122   public function __construct(ConfigFactoryInterface $config_factory, EntityManagerInterface $entity_manager, Connection $connection, TranslationInterface $string_translation, CommentManagerInterface $comment_manager) {
123     $this->configFactory = $config_factory;
124     $this->entityManager = $entity_manager;
125     $this->connection = $connection;
126     $this->stringTranslation = $string_translation;
127     $this->commentManager = $comment_manager;
128   }
129
130   /**
131    * {@inheritdoc}
132    */
133   public function getTopics($tid, AccountInterface $account) {
134     $config = $this->configFactory->get('forum.settings');
135     $forum_per_page = $config->get('topics.page_limit');
136     $sortby = $config->get('topics.order');
137
138     $header = [
139       ['data' => $this->t('Topic'), 'field' => 'f.title'],
140       ['data' => $this->t('Replies'), 'field' => 'f.comment_count'],
141       ['data' => $this->t('Last reply'), 'field' => 'f.last_comment_timestamp'],
142     ];
143
144     $order = $this->getTopicOrder($sortby);
145     for ($i = 0; $i < count($header); $i++) {
146       if ($header[$i]['field'] == $order['field']) {
147         $header[$i]['sort'] = $order['sort'];
148       }
149     }
150
151     $query = $this->connection->select('forum_index', 'f')
152       ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
153       ->extend('Drupal\Core\Database\Query\TableSortExtender');
154     $query->fields('f');
155     $query
156       ->condition('f.tid', $tid)
157       ->addTag('node_access')
158       ->addMetaData('base_table', 'forum_index')
159       ->orderBy('f.sticky', 'DESC')
160       ->orderByHeader($header)
161       ->limit($forum_per_page);
162
163     $count_query = $this->connection->select('forum_index', 'f');
164     $count_query->condition('f.tid', $tid);
165     $count_query->addExpression('COUNT(*)');
166     $count_query->addTag('node_access');
167     $count_query->addMetaData('base_table', 'forum_index');
168
169     $query->setCountQuery($count_query);
170     $result = $query->execute();
171     $nids = [];
172     foreach ($result as $record) {
173       $nids[] = $record->nid;
174     }
175     if ($nids) {
176       $nodes = $this->entityManager->getStorage('node')->loadMultiple($nids);
177
178       $query = $this->connection->select('node_field_data', 'n')
179         ->extend('Drupal\Core\Database\Query\TableSortExtender');
180       $query->fields('n', ['nid']);
181
182       $query->join('comment_entity_statistics', 'ces', "n.nid = ces.entity_id AND ces.field_name = 'comment_forum' AND ces.entity_type = 'node'");
183       $query->fields('ces', [
184         'cid',
185         'last_comment_uid',
186         'last_comment_timestamp',
187         'comment_count',
188       ]);
189
190       $query->join('forum_index', 'f', 'f.nid = n.nid');
191       $query->addField('f', 'tid', 'forum_tid');
192
193       $query->join('users_field_data', 'u', 'n.uid = u.uid AND u.default_langcode = 1');
194       $query->addField('u', 'name');
195
196       $query->join('users_field_data', 'u2', 'ces.last_comment_uid = u2.uid AND u.default_langcode = 1');
197
198       $query->addExpression('CASE ces.last_comment_uid WHEN 0 THEN ces.last_comment_name ELSE u2.name END', 'last_comment_name');
199
200       $query
201         ->orderBy('f.sticky', 'DESC')
202         ->orderByHeader($header)
203         ->condition('n.nid', $nids, 'IN')
204         // @todo This should be actually filtering on the desired node language
205         //   and just fall back to the default language.
206         ->condition('n.default_langcode', 1);
207
208       $result = [];
209       foreach ($query->execute() as $row) {
210         $topic = $nodes[$row->nid];
211         $topic->comment_mode = $topic->comment_forum->status;
212
213         foreach ($row as $key => $value) {
214           $topic->{$key} = $value;
215         }
216         $result[] = $topic;
217       }
218     }
219     else {
220       $result = [];
221     }
222
223     $topics = [];
224     $first_new_found = FALSE;
225     foreach ($result as $topic) {
226       if ($account->isAuthenticated()) {
227         // A forum is new if the topic is new, or if there are new comments since
228         // the user's last visit.
229         if ($topic->forum_tid != $tid) {
230           $topic->new = 0;
231         }
232         else {
233           $history = $this->lastVisit($topic->id(), $account);
234           $topic->new_replies = $this->commentManager->getCountNewComments($topic, 'comment_forum', $history);
235           $topic->new = $topic->new_replies || ($topic->last_comment_timestamp > $history);
236         }
237       }
238       else {
239         // Do not track "new replies" status for topics if the user is anonymous.
240         $topic->new_replies = 0;
241         $topic->new = 0;
242       }
243
244       // Make sure only one topic is indicated as the first new topic.
245       $topic->first_new = FALSE;
246       if ($topic->new != 0 && !$first_new_found) {
247         $topic->first_new = TRUE;
248         $first_new_found = TRUE;
249       }
250
251       if ($topic->comment_count > 0) {
252         $last_reply = new \stdClass();
253         $last_reply->created = $topic->last_comment_timestamp;
254         $last_reply->name = $topic->last_comment_name;
255         $last_reply->uid = $topic->last_comment_uid;
256         $topic->last_reply = $last_reply;
257       }
258       $topics[$topic->id()] = $topic;
259     }
260
261     return ['topics' => $topics, 'header' => $header];
262
263   }
264
265   /**
266    * Gets topic sorting information based on an integer code.
267    *
268    * @param int $sortby
269    *   One of the following integers indicating the sort criteria:
270    *   - ForumManager::NEWEST_FIRST: Date - newest first.
271    *   - ForumManager::OLDEST_FIRST: Date - oldest first.
272    *   - ForumManager::MOST_POPULAR_FIRST: Posts with the most comments first.
273    *   - ForumManager::LEAST_POPULAR_FIRST: Posts with the least comments first.
274    *
275    * @return array
276    *   An array with the following values:
277    *   - field: A field for an SQL query.
278    *   - sort: 'asc' or 'desc'.
279    */
280   protected function getTopicOrder($sortby) {
281     switch ($sortby) {
282       case static::NEWEST_FIRST:
283         return ['field' => 'f.last_comment_timestamp', 'sort' => 'desc'];
284
285       case static::OLDEST_FIRST:
286         return ['field' => 'f.last_comment_timestamp', 'sort' => 'asc'];
287
288       case static::MOST_POPULAR_FIRST:
289         return ['field' => 'f.comment_count', 'sort' => 'desc'];
290
291       case static::LEAST_POPULAR_FIRST:
292         return ['field' => 'f.comment_count', 'sort' => 'asc'];
293
294     }
295   }
296
297   /**
298    * Gets the last time the user viewed a node.
299    *
300    * @param int $nid
301    *   The node ID.
302    * @param \Drupal\Core\Session\AccountInterface $account
303    *   Account to fetch last time for.
304    *
305    * @return int
306    *   The timestamp when the user last viewed this node, if the user has
307    *   previously viewed the node; otherwise HISTORY_READ_LIMIT.
308    */
309   protected function lastVisit($nid, AccountInterface $account) {
310     if (empty($this->history[$nid])) {
311       $result = $this->connection->select('history', 'h')
312         ->fields('h', ['nid', 'timestamp'])
313         ->condition('uid', $account->id())
314         ->execute();
315       foreach ($result as $t) {
316         $this->history[$t->nid] = $t->timestamp > HISTORY_READ_LIMIT ? $t->timestamp : HISTORY_READ_LIMIT;
317       }
318     }
319     return isset($this->history[$nid]) ? $this->history[$nid] : HISTORY_READ_LIMIT;
320   }
321
322   /**
323    * Provides the last post information for the given forum tid.
324    *
325    * @param int $tid
326    *   The forum tid.
327    *
328    * @return \stdClass
329    *   The last post for the given forum.
330    */
331   protected function getLastPost($tid) {
332     if (!empty($this->lastPostData[$tid])) {
333       return $this->lastPostData[$tid];
334     }
335     // Query "Last Post" information for this forum.
336     $query = $this->connection->select('node_field_data', 'n');
337     $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', [':tid' => $tid]);
338     $query->join('comment_entity_statistics', 'ces', "n.nid = ces.entity_id AND ces.field_name = 'comment_forum' AND ces.entity_type = 'node'");
339     $query->join('users_field_data', 'u', 'ces.last_comment_uid = u.uid AND u.default_langcode = 1');
340     $query->addExpression('CASE ces.last_comment_uid WHEN 0 THEN ces.last_comment_name ELSE u.name END', 'last_comment_name');
341
342     $topic = $query
343       ->fields('ces', ['last_comment_timestamp', 'last_comment_uid'])
344       ->condition('n.status', 1)
345       ->orderBy('last_comment_timestamp', 'DESC')
346       ->range(0, 1)
347       ->addTag('node_access')
348       ->execute()
349       ->fetchObject();
350
351     // Build the last post information.
352     $last_post = new \stdClass();
353     if (!empty($topic->last_comment_timestamp)) {
354       $last_post->created = $topic->last_comment_timestamp;
355       $last_post->name = $topic->last_comment_name;
356       $last_post->uid = $topic->last_comment_uid;
357     }
358
359     $this->lastPostData[$tid] = $last_post;
360     return $last_post;
361   }
362
363   /**
364    * Provides statistics for a forum.
365    *
366    * @param int $tid
367    *   The forum tid.
368    *
369    * @return \stdClass|null
370    *   Statistics for the given forum if statistics exist, else NULL.
371    */
372   protected function getForumStatistics($tid) {
373     if (empty($this->forumStatistics)) {
374       // Prime the statistics.
375       $query = $this->connection->select('node_field_data', 'n');
376       $query->join('comment_entity_statistics', 'ces', "n.nid = ces.entity_id AND ces.field_name = 'comment_forum' AND ces.entity_type = 'node'");
377       $query->join('forum', 'f', 'n.vid = f.vid');
378       $query->addExpression('COUNT(n.nid)', 'topic_count');
379       $query->addExpression('SUM(ces.comment_count)', 'comment_count');
380       $this->forumStatistics = $query
381         ->fields('f', ['tid'])
382         ->condition('n.status', 1)
383         ->condition('n.default_langcode', 1)
384         ->groupBy('tid')
385         ->addTag('node_access')
386         ->execute()
387         ->fetchAllAssoc('tid');
388     }
389
390     if (!empty($this->forumStatistics[$tid])) {
391       return $this->forumStatistics[$tid];
392     }
393   }
394
395   /**
396    * {@inheritdoc}
397    */
398   public function getChildren($vid, $tid) {
399     if (!empty($this->forumChildren[$tid])) {
400       return $this->forumChildren[$tid];
401     }
402     $forums = [];
403     $_forums = $this->entityManager->getStorage('taxonomy_term')->loadTree($vid, $tid, NULL, TRUE);
404     foreach ($_forums as $forum) {
405       // Merge in the topic and post counters.
406       if (($count = $this->getForumStatistics($forum->id()))) {
407         $forum->num_topics = $count->topic_count;
408         $forum->num_posts = $count->topic_count + $count->comment_count;
409       }
410       else {
411         $forum->num_topics = 0;
412         $forum->num_posts = 0;
413       }
414
415       // Merge in last post details.
416       $forum->last_post = $this->getLastPost($forum->id());
417       $forums[$forum->id()] = $forum;
418     }
419
420     $this->forumChildren[$tid] = $forums;
421     return $forums;
422   }
423
424   /**
425    * {@inheritdoc}
426    */
427   public function getIndex() {
428     if ($this->index) {
429       return $this->index;
430     }
431
432     $vid = $this->configFactory->get('forum.settings')->get('vocabulary');
433     $index = $this->entityManager->getStorage('taxonomy_term')->create([
434       'tid' => 0,
435       'container' => 1,
436       'parents' => [],
437       'isIndex' => TRUE,
438       'vid' => $vid,
439     ]);
440
441     // Load the tree below.
442     $index->forums = $this->getChildren($vid, 0);
443     $this->index = $index;
444     return $index;
445   }
446
447   /**
448    * {@inheritdoc}
449    */
450   public function resetCache() {
451     // Reset the index.
452     $this->index = NULL;
453     // Reset history.
454     $this->history = [];
455   }
456
457   /**
458    * {@inheritdoc}
459    */
460   public function getParents($tid) {
461     return $this->entityManager->getStorage('taxonomy_term')->loadAllParents($tid);
462   }
463
464   /**
465    * {@inheritdoc}
466    */
467   public function checkNodeType(NodeInterface $node) {
468     // Fetch information about the forum field.
469     $field_definitions = $this->entityManager->getFieldDefinitions('node', $node->bundle());
470     return !empty($field_definitions['taxonomy_forums']);
471   }
472
473   /**
474    * {@inheritdoc}
475    */
476   public function unreadTopics($term, $uid) {
477     $query = $this->connection->select('node_field_data', 'n');
478     $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', [':tid' => $term]);
479     $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', [':uid' => $uid]);
480     $query->addExpression('COUNT(n.nid)', 'count');
481     return $query
482       ->condition('status', 1)
483       // @todo This should be actually filtering on the desired node status
484       //   field language and just fall back to the default language.
485       ->condition('n.default_langcode', 1)
486       ->condition('n.created', HISTORY_READ_LIMIT, '>')
487       ->isNull('h.nid')
488       ->addTag('node_access')
489       ->execute()
490       ->fetchField();
491   }
492
493   /**
494    * {@inheritdoc}
495    */
496   public function __sleep() {
497     $vars = $this->defaultSleep();
498     // Do not serialize static cache.
499     unset($vars['history'], $vars['index'], $vars['lastPostData'], $vars['forumChildren'], $vars['forumStatistics']);
500     return $vars;
501   }
502
503   /**
504    * {@inheritdoc}
505    */
506   public function __wakeup() {
507     $this->defaultWakeup();
508     // Initialize static cache.
509     $this->history = [];
510     $this->lastPostData = [];
511     $this->forumChildren = [];
512     $this->forumStatistics = [];
513     $this->index = NULL;
514   }
515
516 }