comment.module 62 KB
Newer Older
1
<?php
Dries's avatar
 
Dries committed
2

Dries's avatar
Dries committed
3
4
/**
 * @file
Dries's avatar
   
Dries committed
5
 * Enables users to comment on published content.
Dries's avatar
Dries committed
6
 *
7
8
9
 * When enabled, the Comment module creates a discussion board for each Drupal
 * node. Users can post comments to discuss a forum topic, story, collaborative
 * book page, etc.
Dries's avatar
Dries committed
10
11
 */

12
use Drupal\node\NodeTypeInterface;
13
14
use Drupal\entity\Entity\EntityDisplay;
use Drupal\file\Entity\File;
15
use Drupal\Core\Entity\EntityInterface;
16
use Drupal\node\NodeInterface;
17
use Symfony\Component\HttpFoundation\Request;
18
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
19
use Symfony\Component\HttpKernel\HttpKernelInterface;
20

21
/**
22
 * Comment is awaiting approval.
23
 */
24
const COMMENT_NOT_PUBLISHED = 0;
25
26

/**
27
 * Comment is published.
28
 */
29
const COMMENT_PUBLISHED = 1;
Dries's avatar
Dries committed
30

31
32
33
/**
 * Comments are displayed in a flat list - expanded.
 */
34
const COMMENT_MODE_FLAT = 0;
35
36
37
38

/**
 * Comments are displayed as a threaded list - expanded.
 */
39
const COMMENT_MODE_THREADED = 1;
Dries's avatar
Dries committed
40
41

/**
42
 * Anonymous posters cannot enter their contact information.
Dries's avatar
Dries committed
43
 */
44
const COMMENT_ANONYMOUS_MAYNOT_CONTACT = 0;
45
46
47
48

/**
 * Anonymous posters may leave their contact information.
 */
49
const COMMENT_ANONYMOUS_MAY_CONTACT = 1;
50
51

/**
52
 * Anonymous posters are required to leave their contact information.
53
 */
54
const COMMENT_ANONYMOUS_MUST_CONTACT = 2;
Dries's avatar
Dries committed
55
56

/**
57
 * Comment form should be displayed on a separate page.
Dries's avatar
Dries committed
58
 */
59
const COMMENT_FORM_SEPARATE_PAGE = 0;
60
61
62
63

/**
 * Comment form should be shown below post or list of comments.
 */
64
const COMMENT_FORM_BELOW = 1;
Dries's avatar
Dries committed
65
66

/**
67
 * Comments for this node are hidden.
Dries's avatar
Dries committed
68
 */
69
const COMMENT_NODE_HIDDEN = 0;
70
71

/**
72
 * Comments for this node are closed.
73
 */
74
const COMMENT_NODE_CLOSED = 1;
75
76

/**
77
 * Comments for this node are open.
78
 */
79
const COMMENT_NODE_OPEN = 2;
80

81
use Drupal\comment\Entity\Comment;
82

83
/**
84
 * Implements hook_help().
85
 */
86
87
function comment_help($path, $arg) {
  switch ($path) {
Dries's avatar
   
Dries committed
88
    case 'admin/help#comment':
89
      $output = '<h3>' . t('About') . '</h3>';
90
      $output .= '<p>' . t('The Comment module allows users to comment on site content, set commenting defaults and permissions, and moderate comments. For more information, see the online handbook entry for <a href="@comment">Comment module</a>.', array('@comment' => 'http://drupal.org/documentation/modules/comment')) . '</p>';
91
92
93
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Default and custom settings') . '</dt>';
94
      $output .= '<dd>' . t("Each <a href='@content-type'>content type</a> can have its own default comment settings configured as: <em>Open</em> to allow new comments, <em>Hidden</em> to hide existing comments and prevent new comments, or <em>Closed</em> to view existing comments, but prevent new comments. These defaults will apply to all new content created (changes to the settings on existing content must be done manually). Other comment settings can also be customized per content type, and can be overridden for any given item of content. When a comment has no replies, it remains editable by its author, as long as the author has a user account and is logged in.", array('@content-type' => url('admin/structure/types'))) . '</dd>';
95
96
      $output .= '<dt>' . t('Comment approval') . '</dt>';
      $output .= '<dd>' . t("Comments from users who have the <em>Skip comment approval</em> permission are published immediately. All other comments are placed in the <a href='@comment-approval'>Unapproved comments</a> queue, until a user who has permission to <em>Administer comments</em> publishes or deletes them. Published comments can be bulk managed on the <a href='@admin-comment'>Published comments</a> administration page.", array('@comment-approval' => url('admin/content/comment/approval'), '@admin-comment' => url('admin/content/comment'))) . '</dd>';
97
      $output .= '</dl>';
98
      return $output;
99
  }
Dries's avatar
   
Dries committed
100
101
}

102
103
104
105
106
/**
 * Implements hook_entity_bundle_info().
 */
function comment_entity_bundle_info() {
  $bundles = array();
107
  foreach (node_type_get_names() as $type => $name) {
108
    $bundles['comment']['comment_node_' . $type] = array(
109
      'label' => t('@node_type comment', array('@node_type' => $name)),
110
111
112
      // Provide the node type/bundle name for other modules, so it does not
      // have to be extracted manually from the bundle name.
      'node bundle' => $type,
113
114
    );
  }
115
  return $bundles;
116
117
}

118
/**
119
 * Entity URI callback.
120
 */
121
function comment_uri(Comment $comment) {
122
  return array(
123
124
    'path' => 'comment/' . $comment->id(),
    'options' => array('fragment' => 'comment-' . $comment->id()),
125
  );
126
127
}

128
129
130
131
132
133
134
135
136
/**
 * Implements hook_field_extra_fields().
 */
function comment_field_extra_fields() {
  $return = array();

  foreach (node_type_get_types() as $type) {
    if (variable_get('comment_subject_field_' . $type->type, 1) == 1) {
      $return['comment']['comment_node_' . $type->type] = array(
137
138
139
140
141
142
        'form' => array(
          'author' => array(
            'label' => t('Author'),
            'description' => t('Author textfield'),
            'weight' => -2,
          ),
143
          'subject' => array(
144
145
146
147
            'label' => t('Subject'),
            'description' => t('Subject textfield'),
            'weight' => -1,
          ),
148
149
150
151
152
153
154
155
        ),
      );
    }
  }

  return $return;
}

156
/**
157
 * Implements hook_theme().
158
159
160
161
 */
function comment_theme() {
  return array(
    'comment_block' => array(
162
      'variables' => array('number' => NULL),
163
164
    ),
    'comment_preview' => array(
165
      'variables' => array('comment' => NULL),
166
167
    ),
    'comment' => array(
168
      'template' => 'comment',
169
      'render element' => 'elements',
170
171
    ),
    'comment_post_forbidden' => array(
172
      'variables' => array('node' => NULL),
173
174
    ),
    'comment_wrapper' => array(
175
      'template' => 'comment-wrapper',
176
      'render element' => 'content',
177
178
179
180
    ),
  );
}

Dries's avatar
   
Dries committed
181
/**
182
 * Implements hook_menu().
Dries's avatar
   
Dries committed
183
 */
184
function comment_menu() {
185
  $items['admin/content/comment'] = array(
186
    'title' => 'Comments',
187
    'description' => 'List and edit site comments and the comment approval queue.',
188
    'route_name' => 'comment.admin',
189
    'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM,
190
  );
191
  // Tabs begin here.
192
  $items['admin/content/comment/new'] = array(
193
    'title' => 'Published comments',
194
195
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
196
  $items['admin/content/comment/approval'] = array(
197
198
    'title' => 'Unapproved comments',
    'title callback' => 'comment_count_unpublished',
199
    'route_name' => 'comment.admin_approval',
200
201
    'type' => MENU_LOCAL_TASK,
  );
202
  $items['comment/%comment'] = array(
203
    'title' => 'Comment permalink',
204
    'route_name' => 'comment.permalink',
205
  );
206
  $items['comment/%comment/view'] = array(
207
208
209
    'title' => 'View comment',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
210
211
  // Every other comment path uses %, but this one loads the comment directly,
  // so we don't end up loading it twice (in the page and access callback).
212
213
214
  $items['comment/%comment/edit'] = array(
    'title' => 'Edit',
    'type' => MENU_LOCAL_TASK,
215
    'route_name' => 'comment.edit_page',
216
  );
217
  $items['comment/%comment/approve'] = array(
218
    'title' => 'Approve',
219
    'weight' => 10,
220
    'route_name' => 'comment.approve',
221
  );
222
  $items['comment/%comment/delete'] = array(
223
224
    'title' => 'Delete',
    'type' => MENU_LOCAL_TASK,
225
    'route_name' => 'comment.confirm_delete',
226
    'weight' => 20,
227
  );
Dries's avatar
   
Dries committed
228
229
230
231

  return $items;
}

232
233
234
235
236
/**
 * Implements hook_menu_alter().
 */
function comment_menu_alter(&$items) {
  // Add comments to the description for admin/content.
237
  $items['admin/content']['description'] = 'Administer content and comments.';
238
239

  // Adjust the Field UI tabs on admin/structure/types/manage/[node-type].
240
  // See comment_entity_bundle_info().
241
242
  $items['admin/structure/types/manage/{bundle}/comment/fields']['title'] = 'Comment fields';
  $items['admin/structure/types/manage/{bundle}/comment/fields']['weight'] = 3;
243
244
  $items['admin/structure/types/manage/{bundle}/comment/form-display']['title'] = 'Comment form display';
  $items['admin/structure/types/manage/{bundle}/comment/form-display']['weight'] = 4;
245
  $items['admin/structure/types/manage/{bundle}/comment/display']['title'] = 'Comment display';
246
  $items['admin/structure/types/manage/{bundle}/comment/display']['weight'] = 5;
247
248
}

249
250
251
252
253
254
255
256
257
258
/**
 * Returns a menu title which includes the number of unapproved comments.
 */
function comment_count_unpublished() {
  $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE status = :status', array(
    ':status' => COMMENT_NOT_PUBLISHED,
  ))->fetchField();
  return t('Unapproved comments (@count)', array('@count' => $count));
}

259
/**
260
 * Implements hook_node_type_insert().
261
 *
262
263
 * Creates a comment body field for a node type created while the Comment module
 * is enabled. For node types created before the Comment module is enabled,
264
265
266
 * hook_modules_enabled() serves to create the body fields.
 *
 * @see comment_modules_enabled()
267
 */
268
function comment_node_type_insert($info) {
269
  _comment_body_field_create($info);
270
}
271

272
/**
273
 * Implements hook_node_type_update().
274
 */
275
276
277
function comment_node_type_update(NodeTypeInterface $type) {
  if ($type->original->id() != $type->id()) {
    entity_invoke_bundle_hook('rename', 'comment', 'comment_node_' . $type->original->id(), 'comment_node_' . $type->id());
278
279
  }
}
280

281
/**
282
 * Implements hook_node_type_delete().
283
284
 */
function comment_node_type_delete($info) {
285
  entity_invoke_bundle_hook('delete', 'comment', 'comment_node_' . $info->type);
286
287
288
289
290
291
292
293
294
295
296
  $settings = array(
    'comment',
    'comment_default_mode',
    'comment_default_per_page',
    'comment_anonymous',
    'comment_subject_field',
    'comment_preview',
    'comment_form_location',
  );
  foreach ($settings as $setting) {
    variable_del($setting . '_' . $info->type);
297
298
299
  }
}

300
 /**
301
 * Creates a comment_body field instance for a given node type.
302
303
304
305
306
 *
 * @param $info
 *   An object representing the content type. The only property that is
 *   currently used is $info->type, which is the machine name of the content
 *   type for which the body field (instance) is to be created.
307
 */
308
309
function _comment_body_field_create($info) {
  // Create the field if needed.
310
  if (!field_read_field('comment', 'comment_body', array('include_inactive' => TRUE))) {
311
    $field = entity_create('field_entity', array(
312
      'name' => 'comment_body',
313
      'type' => 'text_long',
314
      'entity_type' => 'comment',
315
316
    ));
    $field->save();
317
318
319
  }
  // Create the instance if needed.
  if (!field_read_instance('comment', 'comment_body', 'comment_node_' . $info->type, array('include_inactive' => TRUE))) {
320
    entity_invoke_bundle_hook('create', 'comment', 'comment_node_' . $info->type);
321
    // Attaches the body field by default.
322
    $instance = entity_create('field_instance', array(
323
324
325
326
327
328
      'field_name' => 'comment_body',
      'label' => 'Comment',
      'entity_type' => 'comment',
      'bundle' => 'comment_node_' . $info->type,
      'settings' => array('text_processing' => 1),
      'required' => TRUE,
329
330
    ));
    $instance->save();
331
332
333
334
335
336
337
338
339

    // Assign widget settings for the 'default' form mode.
    entity_get_form_display('comment', 'comment_node_' . $info->type, 'default')
      ->setComponent('comment_body', array(
        'type' => 'text_textarea',
      ))
      ->save();

    // Assign display settings for the 'default' view mode.
340
341
342
343
344
345
346
    entity_get_display('comment', 'comment_node_' . $info->type, 'default')
      ->setComponent('comment_body', array(
        'label' => 'hidden',
        'type' => 'text_default',
        'weight' => 0,
      ))
      ->save();
347
  }
348
349
}

Dries's avatar
   
Dries committed
350
/**
351
 * Implements hook_permission().
Dries's avatar
   
Dries committed
352
 */
353
function comment_permission() {
354
  return array(
355
    'administer comments' => array(
356
      'title' => t('Administer comments and comment settings'),
357
358
    ),
    'access comments' => array(
359
      'title' => t('View comments'),
360
361
    ),
    'post comments' => array(
362
      'title' => t('Post comments'),
363
    ),
364
365
    'skip comment approval' => array(
      'title' => t('Skip comment approval'),
366
    ),
367
368
369
    'edit own comments' => array(
      'title' => t('Edit own comments'),
    ),
370
  );
Dries's avatar
   
Dries committed
371
372
}

373
/**
374
 * Finds the most recent comments that are available to the current user.
375
376
 *
 * @param integer $number
377
 *   (optional) The maximum number of comments to find. Defaults to 10.
378
 *
379
 * @return
380
381
 *   An array of comment objects or an empty array if there are no recent
 *   comments visible to the current user.
382
383
 */
function comment_get_recent($number = 10) {
384
  $query = db_select('comment', 'c');
385
  $query->innerJoin('node_field_data', 'n', 'n.nid = c.nid');
386
  $query->addTag('node_access');
387
  $query->addMetaData('base_table', 'comment');
388
389
390
391
  $comments = $query
    ->fields('c')
    ->condition('c.status', COMMENT_PUBLISHED)
    ->condition('n.status', NODE_PUBLISHED)
392
393
394
    // @todo This should be actually filtering on the desired node status field
    //   language and just fall back to the default language.
    ->condition('n.default_langcode', 1)
395
    ->orderBy('c.created', 'DESC')
396
397
398
    // Additionally order by cid to ensure that comments with the same timestamp
    // are returned in the exact order posted.
    ->orderBy('c.cid', 'DESC')
399
400
401
402
403
    ->range(0, $number)
    ->execute()
    ->fetchAll();

  return $comments ? $comments : array();
404
405
}

406
/**
407
 * Calculates the page number for the first new comment.
408
409
410
411
412
 *
 * @param $num_comments
 *   Number of comments.
 * @param $new_replies
 *   Number of new replies.
413
 * @param \Drupal\Core\Entity\EntityInterface $node
414
 *   The first new comment node.
415
 *
416
417
 * @return
 *   "page=X" if the page number is greater than zero; empty string otherwise.
418
 */
419
function comment_new_page_count($num_comments, $new_replies, EntityInterface $node) {
420
421
  $mode = variable_get('comment_default_mode_' . $node->getType(), COMMENT_MODE_THREADED);
  $comments_per_page = variable_get('comment_default_per_page_' . $node->getType(), 50);
422
  $pagenum = NULL;
423
  $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
424
425
  if ($num_comments <= $comments_per_page) {
    // Only one page of comments.
426
    $pageno = 0;
427
  }
428
429
430
  elseif ($flat) {
    // Flat comments.
    $count = $num_comments - $new_replies;
431
    $pageno = $count / $comments_per_page;
432
  }
433
  else {
434
435
436
437
438
439
    // Threaded comments: we build a query with a subquery to find the first
    // thread with a new comment.

    // 1. Find all the threads with a new comment.
    $unread_threads_query = db_select('comment')
      ->fields('comment', array('thread'))
440
      ->condition('nid', $node->id())
441
      ->condition('status', COMMENT_PUBLISHED)
442
443
      ->orderBy('created', 'DESC')
      ->orderBy('cid', 'DESC')
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
      ->range(0, $new_replies);

    // 2. Find the first thread.
    $first_thread = db_select($unread_threads_query, 'thread')
      ->fields('thread', array('thread'))
      ->orderBy('SUBSTRING(thread, 1, (LENGTH(thread) - 1))')
      ->range(0, 1)
      ->execute()
      ->fetchField();

    // Remove the final '/'.
    $first_thread = substr($first_thread, 0, -1);

    // Find the number of the first comment of the first unread thread.
    $count = db_query('SELECT COUNT(*) FROM {comment} WHERE nid = :nid AND status = :status AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread', array(
      ':status' => COMMENT_PUBLISHED,
460
      ':nid' => $node->id(),
461
      ':thread' => $first_thread,
462
    ))->fetchField();
463

464
    $pageno = $count / $comments_per_page;
465
  }
466

467
  if ($pageno >= 1) {
468
    $pagenum = array('page' => intval($pageno));
469
  }
470

471
472
473
  return $pagenum;
}

474
/**
475
 * Returns HTML for a list of recent comments.
476
477
478
 *
 * @ingroup themeable
 */
479
function theme_comment_block($variables) {
480
  $items = array();
481
  $number = $variables['number'];
482
  foreach (comment_get_recent($number) as $comment) {
483
    $items[] = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)) . '&nbsp;<span>' . t('@time ago', array('@time' => format_interval(REQUEST_TIME - $comment->changed))) . '</span>';
484
  }
485

486
  if ($items) {
487
488
489
490
491
    $item_list = array(
      '#theme' => 'item_list',
      '#items' => $items,
    );
    return drupal_render($item_list);
492
  }
493
494
495
  else {
    return t('No comments available.');
  }
496
497
}

Dries's avatar
   
Dries committed
498
/**
499
 * Implements hook_node_view().
Dries's avatar
   
Dries committed
500
 */
501
function comment_node_view(EntityInterface $node, EntityDisplay $display, $view_mode) {
Dries's avatar
   
Dries committed
502
503
  $links = array();

504
  if ($node->comment->value != COMMENT_NODE_HIDDEN) {
505
    if ($view_mode == 'rss') {
506
507
508
      // Add a comments RSS element which is a URL to the comments of this node.
      $node->rss_elements[] = array(
        'key' => 'comments',
509
        'value' => url('node/' . $node->id(), array('fragment' => 'comments', 'absolute' => TRUE))
510
      );
511
    }
512
    elseif ($view_mode == 'teaser') {
513
514
515
      // Teaser view: display the number of comments that have been posted,
      // or a link to add new comments if the user has permission, the node
      // is open to new comments, and there currently are none.
Dries's avatar
   
Dries committed
516
      if (user_access('access comments')) {
517
        if (!empty($node->comment_count)) {
518
          $links['comment-comments'] = array(
519
            'title' => format_plural($node->comment_count, '1 comment', '@count comments'),
520
            'href' => 'node/' . $node->id(),
521
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
522
523
            'fragment' => 'comments',
            'html' => TRUE,
524
          );
525
          if (\Drupal::moduleHandler()->moduleExists('history')) {
526
            $links['comment-new-comments'] = array(
527
528
529
530
531
532
533
              'title' => '',
              'href' => '',
              'attributes' => array(
                'class' => 'hidden',
                'title' => t('Jump to the first new comment of this posting.'),
                'data-history-node-last-comment-timestamp' => $node->last_comment_timestamp,
              ),
534
              'html' => TRUE,
535
            );
Dries's avatar
   
Dries committed
536
537
          }
        }
538
      }
539
      if ($node->comment->value == COMMENT_NODE_OPEN) {
540
        $comment_form_location = variable_get('comment_form_location_' . $node->getType(), COMMENT_FORM_BELOW);
541
542
543
        if (user_access('post comments')) {
          $links['comment-add'] = array(
            'title' => t('Add new comment'),
544
            'href' => 'node/' . $node->id(),
545
546
547
            'attributes' => array('title' => t('Add a new comment to this page.')),
            'fragment' => 'comment-form',
          );
548
          if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
549
            $links['comment-add']['href'] = 'comment/reply/' . $node->id();
550
          }
551
        }
Dries's avatar
   
Dries committed
552
        else {
553
554
555
556
          $comment_post_forbidden = array(
            '#theme' => 'comment_post_forbidden',
            '#node' => $node,
          );
557
          $links['comment-forbidden'] = array(
558
            'title' => drupal_render($comment_post_forbidden),
559
560
            'html' => TRUE,
          );
Dries's avatar
   
Dries committed
561
562
563
        }
      }
    }
564
    elseif ($view_mode != 'search_index' && $view_mode != 'search_result') {
565
566
567
      // Node in other view modes: add a "post comment" link if the user is
      // allowed to post comments and if this node is allowing new comments.
      // But we don't want this link if we're building the node for search
568
      // indexing or constructing a search result excerpt.
569
      if ($node->comment->value == COMMENT_NODE_OPEN) {
570
        $comment_form_location = variable_get('comment_form_location_' . $node->getType(), COMMENT_FORM_BELOW);
Dries's avatar
   
Dries committed
571
        if (user_access('post comments')) {
572
573
574
575
576
577
          // Show the "post comment" link if the form is on another page, or
          // if there are existing comments that the link will skip past.
          if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE || (!empty($node->comment_count) && user_access('access comments'))) {
            $links['comment-add'] = array(
              'title' => t('Add new comment'),
              'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
578
              'href' => 'node/' . $node->id(),
579
580
581
              'fragment' => 'comment-form',
            );
            if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
582
              $links['comment-add']['href'] = 'comment/reply/' . $node->id();
583
            }
584
          }
Dries's avatar
   
Dries committed
585
586
        }
        else {
587
588
589
590
          $comment_post_forbidden = array(
            '#theme' => 'comment_post_forbidden',
            '#node' => $node,
          );
591
          $links['comment-forbidden'] = array(
592
            'title' => drupal_render($comment_post_forbidden),
593
594
            'html' => TRUE,
          );
Dries's avatar
   
Dries committed
595
596
597
        }
      }
    }
Dries's avatar
Dries committed
598

599
600
601
602
603
    $node->content['links']['comment'] = array(
      '#theme' => 'links__node__comment',
      '#links' => $links,
      '#attributes' => array('class' => array('links', 'inline')),
    );
604
    if ($view_mode == 'teaser' && \Drupal::moduleHandler()->moduleExists('history')) {
605
606
      $node->content['links']['#attached']['library'][] = array('comment', 'drupal.node-new-comments-link');
    }
Dries's avatar
Dries committed
607

608
609
610
    // Only append comments when we are building a node on its own node detail
    // page. We compare $node and $page_node to ensure that comments are not
    // appended to other nodes shown on the page, for example a node_reference
611
    // displayed in 'full' view mode within another node.
612
    if ($node->comment && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) {
613
      $node->content['comments'] = comment_node_page_additions($node);
614
    }
615
  }
Dries's avatar
   
Dries committed
616
617
}

618
619
620
621
/**
 * Implements hook_node_view_alter().
 */
function comment_node_view_alter(&$build, EntityInterface $node, EntityDisplay $display) {
622
  if (\Drupal::moduleHandler()->moduleExists('history')) {
623
624
625
626
    $build['#attributes']['data-history-node-id'] = $node->id();
  }
}

627
/**
628
 * Builds the comment-related elements for node detail pages.
629
 *
630
 * @param \Drupal\Core\Entity\EntityInterface $node
631
 *   The node entity for which to build the comment-related elements.
632
633
634
635
 *
 * @return
 *   A renderable array representing the comment-related page elements for the
 *   node.
636
 */
637
function comment_node_page_additions(EntityInterface $node) {
638
639
640
641
642
  $additions = array();

  // Only attempt to render comments if the node has visible comments.
  // Unpublished comments are not included in $node->comment_count, so show
  // comments unconditionally if the user is an administrator.
643
  if (($node->comment_count && user_access('access comments')) || user_access('administer comments')) {
644
645
    $mode = variable_get('comment_default_mode_' . $node->getType(), COMMENT_MODE_THREADED);
    $comments_per_page = variable_get('comment_default_per_page_' . $node->getType(), 50);
646
    if ($cids = comment_get_thread($node, $mode, $comments_per_page)) {
647
648
      $comments = comment_load_multiple($cids);
      comment_prepare_thread($comments);
649
      $build = comment_view_multiple($comments);
650
651
652
653
654
655
      $build['pager']['#theme'] = 'pager';
      $additions['comments'] = $build;
    }
  }

  // Append comment form if needed.
656
  if (user_access('post comments') && $node->comment->value == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->getType(), COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) {
657
    $additions['comment_form'] = comment_add($node);
658
659
660
661
  }

  if ($additions) {
    $additions += array(
662
      '#theme' => 'comment_wrapper__node_' . $node->getType(),
663
664
665
666
667
668
669
670
671
      '#node' => $node,
      'comments' => array(),
      'comment_form' => array(),
    );
  }

  return $additions;
}

672
673
674
/**
 * Returns a rendered form to comment the given node.
 *
675
 * @param \Drupal\Core\Entity\EntityInterface $node
676
 *   The node entity to be commented.
677
678
679
 * @param int $pid
 *   (optional) Some comments are replies to other comments. In those cases,
 *   $pid is the parent comment's comment ID. Defaults to NULL.
680
681
682
683
 *
 * @return array
 *   The renderable array for the comment addition form.
 */
684
function comment_add(EntityInterface $node, $pid = NULL) {
685
  $values = array('nid' => $node->id(), 'pid' => $pid, 'node_type' => 'comment_node_' . $node->getType());
686
  $comment = entity_create('comment', $values);
687
  return \Drupal::entityManager()->getForm($comment);
688
689
}

690
/**
691
 * Retrieves comments for a thread.
692
 *
693
 * @param \Drupal\Core\Entity\EntityInterface $node
694
 *   The node whose comment(s) needs rendering.
695
696
697
698
 * @param $mode
 *   The comment display mode; COMMENT_MODE_FLAT or COMMENT_MODE_THREADED.
 * @param $comments_per_page
 *   The amount of comments to display per page.
699
 *
700
701
702
 * @return
 *   An array of the IDs of the comment to be displayed.
 *
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
 * To display threaded comments in the correct order we keep a 'thread' field
 * and order by that value. This field keeps this data in
 * a way which is easy to update and convenient to use.
 *
 * A "thread" value starts at "1". If we add a child (A) to this comment,
 * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
 * brother of (A) will get "1.2". Next brother of the parent of (A) will get
 * "2" and so on.
 *
 * First of all note that the thread field stores the depth of the comment:
 * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
 *
 * Now to get the ordering right, consider this example:
 *
 * 1
 * 1.1
 * 1.1.1
 * 1.2
 * 2
 *
 * If we "ORDER BY thread ASC" we get the above result, and this is the
 * natural order sorted by time. However, if we "ORDER BY thread DESC"
 * we get:
 *
 * 2
 * 1.2
 * 1.1.1
 * 1.1
 * 1
 *
 * Clearly, this is not a natural way to see a thread, and users will get
 * confused. The natural order to show a thread by time desc would be:
 *
 * 2
 * 1
 * 1.2
 * 1.1
 * 1.1.1
 *
 * which is what we already did before the standard pager patch. To achieve
 * this we simply add a "/" at the end of each "thread" value. This way, the
 * thread fields will look like this:
 *
 * 1/
 * 1.1/
 * 1.1.1/
 * 1.2/
 * 2/
 *
 * we add "/" since this char is, in ASCII, higher than every number, so if
 * now we "ORDER BY thread DESC" we get the correct order. However this would
 * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
 * to consider the trailing "/" so we use a substring only.
 */
757
function comment_get_thread(EntityInterface $node, $mode, $comments_per_page) {
758
759
  $query = db_select('comment', 'c')
    ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
760
761
  $query->addField('c', 'cid');
  $query
762
    ->condition('c.nid', $node->id())
763
    ->addTag('node_access')
764
    ->addTag('comment_filter')
765
    ->addMetaData('base_table', 'comment')
766
    ->addMetaData('node', $node)
767
768
769
770
771
    ->limit($comments_per_page);

  $count_query = db_select('comment', 'c');
  $count_query->addExpression('COUNT(*)');
  $count_query
772
    ->condition('c.nid', $node->id())
773
774
    ->addTag('node_access')
    ->addTag('comment_filter')
775
    ->addMetaData('base_table', 'comment')
776
    ->addMetaData('node', $node);
777
778
779
780
781
782
783
784
785
786
787
788

  if (!user_access('administer comments')) {
    $query->condition('c.status', COMMENT_PUBLISHED);
    $count_query->condition('c.status', COMMENT_PUBLISHED);
  }
  if ($mode === COMMENT_MODE_FLAT) {
    $query->orderBy('c.cid', 'ASC');
  }
  else {
    // See comment above. Analysis reveals that this doesn't cost too
    // much. It scales much much better than having the whole comment
    // structure.
789
790
    $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
    $query->orderBy('torder', 'ASC');
791
792
793
794
795
796
797
798
799
  }

  $query->setCountQuery($count_query);
  $cids = $query->execute()->fetchCol();

  return $cids;
}

/**
800
801
802
803
804
 * Calculates the indentation level of each comment in a comment thread.
 *
 * This function loops over an array representing a comment thread. For each
 * comment, the function calculates the indentation level and saves it in the
 * 'divs' property of the comment object.
805
806
 *
 * @param array $comments
807
 *   An array of comment objects, keyed by comment ID.
808
809
810
811
812
813
814
815
 */
function comment_prepare_thread(&$comments) {
  // A counter that helps track how indented we are.
  $divs = 0;

  foreach ($comments as $key => $comment) {
    // The $divs element instructs #prefix whether to add an indent div or
    // close existing divs (a negative value).
816
    $comment->depth = count(explode('.', $comment->thread->value)) - 1;
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
    if ($comment->depth > $divs) {
      $comment->divs = 1;
      $divs++;
    }
    else {
      $comment->divs = $comment->depth - $divs;
      while ($comment->depth < $divs) {
        $divs--;
      }
    }
    $comments[$key] = $comment;
  }

  // The final comment must close up some hanging divs
  $comments[$key]->divs_final = $divs;
}

/**
835
 * Generates an array for rendering a comment.
836
 *
837
 * @param Drupal\comment\Comment $comment
838
 *   The comment object.
839
840
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
841
842
843
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
844
845
846
847
 *
 * @return
 *   An array as expected by drupal_render().
 */
848
849
function comment_view(Comment $comment, $view_mode = 'full', $langcode = NULL) {
  return entity_view($comment, $view_mode, $langcode);
850
851
}

852
/**
853
 * Adds reply, edit, delete, etc. links, depending on user permissions.
854
 *
855
 * @param Drupal\comment\Comment $comment
856
 *   The comment object.
857
 * @param \Drupal\Core\Entity\EntityInterface $node
858
 *   The node the comment is attached to.
859
 *
860
861
862
 * @return
 *   A structured array of links.
 */
863
function comment_links(Comment $comment, EntityInterface $node) {
864
  $links = array();
865
  if ($node->comment->value == COMMENT_NODE_OPEN) {
866
    if ($comment->access('delete')) {
867
      $links['comment-delete'] = array(
868
        'title' => t('delete'),
869
        'href' => "comment/{$comment->id()}/delete",
870
871
872
        'html' => TRUE,
      );
    }
873
    if ($comment->access('update')) {
874
        $links['comment-edit'] = array(
875
          'title' => t('edit'),
876
          'href' => "comment/{$comment->id()}/edit",
877
878
          'html' => TRUE,
        );
879
880
    }
    if ($comment->access('create')) {