comment.module 61.4 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 189
    'page callback' => 'comment_admin',
    'access arguments' => array('administer comments'),
190
    'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM,
191
    'file' => 'comment.admin.inc',
192
  );
193
  // Tabs begin here.
194
  $items['admin/content/comment/new'] = array(
195
    'title' => 'Published comments',
196 197
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
198
  $items['admin/content/comment/approval'] = array(
199 200
    'title' => 'Unapproved comments',
    'title callback' => 'comment_count_unpublished',
201
    'page arguments' => array('approval'),
202
    'access arguments' => array('administer comments'),
203 204
    'type' => MENU_LOCAL_TASK,
  );
205
  $items['comment/%comment'] = array(
206
    'title' => 'Comment permalink',
207
    'route_name' => 'comment_permalink',
208
  );
209
  $items['comment/%comment/view'] = array(
210 211 212
    'title' => 'View comment',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
213 214
  // 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).
215 216 217
  $items['comment/%comment/edit'] = array(
    'title' => 'Edit',
    'type' => MENU_LOCAL_TASK,
218
    'route_name' => 'comment_edit_page',
219
  );
220
  $items['comment/%comment/approve'] = array(
221
    'title' => 'Approve',
222
    'weight' => 10,
223
    'route_name' => 'comment_approve',
224
  );
225
  $items['comment/%comment/delete'] = array(
226
    'title' => 'Delete',
227 228
    'page callback' => 'comment_confirm_delete_page',
    'page arguments' => array(1),
229 230
    'access callback' => 'entity_page_access',
    'access arguments' => array(1, 'delete'),
231 232
    'type' => MENU_LOCAL_TASK,
    'file' => 'comment.admin.inc',
233
    'weight' => 20,
234
  );
235
  $items['comment/reply/%node'] = array(
236
    'title' => 'Add new comment',
237
    'page callback' => 'comment_reply',
238
    'page arguments' => array(2),
239 240
    'access callback' => 'node_access',
    'access arguments' => array('view', 2),
241
    'file' => 'comment.pages.inc',
242
  );
Dries's avatar
 
Dries committed
243 244 245 246

  return $items;
}

247 248 249 250 251
/**
 * Implements hook_menu_alter().
 */
function comment_menu_alter(&$items) {
  // Add comments to the description for admin/content.
252
  $items['admin/content']['description'] = 'Administer content and comments.';
253 254

  // Adjust the Field UI tabs on admin/structure/types/manage/[node-type].
255
  // See comment_entity_bundle_info().
256 257
  $items['admin/structure/types/manage/{bundle}/comment/fields']['title'] = 'Comment fields';
  $items['admin/structure/types/manage/{bundle}/comment/fields']['weight'] = 3;
258 259
  $items['admin/structure/types/manage/{bundle}/comment/form-display']['title'] = 'Comment form display';
  $items['admin/structure/types/manage/{bundle}/comment/form-display']['weight'] = 4;
260
  $items['admin/structure/types/manage/{bundle}/comment/display']['title'] = 'Comment display';
261
  $items['admin/structure/types/manage/{bundle}/comment/display']['weight'] = 5;
262 263
}

264 265 266 267 268 269 270 271 272 273
/**
 * 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));
}

274
/**
275
 * Implements hook_node_type_insert().
276
 *
277 278
 * 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,
279 280 281
 * hook_modules_enabled() serves to create the body fields.
 *
 * @see comment_modules_enabled()
282
 */
283
function comment_node_type_insert($info) {
284
  _comment_body_field_create($info);
285
}
286

287
/**
288
 * Implements hook_node_type_update().
289
 */
290 291 292
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());
293 294
  }
}
295

296
/**
297
 * Implements hook_node_type_delete().
298 299
 */
function comment_node_type_delete($info) {
300
  entity_invoke_bundle_hook('delete', 'comment', 'comment_node_' . $info->type);
301 302 303 304 305 306 307 308 309 310 311
  $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);
312 313 314
  }
}

315
 /**
316
 * Creates a comment_body field instance for a given node type.
317 318 319 320 321
 *
 * @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.
322
 */
323 324 325
function _comment_body_field_create($info) {
  // Create the field if needed.
  if (!field_read_field('comment_body', array('include_inactive' => TRUE))) {
326
    $field = entity_create('field_entity', array(
327 328 329
      'field_name' => 'comment_body',
      'type' => 'text_long',
      'entity_types' => array('comment'),
330 331
    ));
    $field->save();
332 333 334
  }
  // Create the instance if needed.
  if (!field_read_instance('comment', 'comment_body', 'comment_node_' . $info->type, array('include_inactive' => TRUE))) {
335
    entity_invoke_bundle_hook('create', 'comment', 'comment_node_' . $info->type);
336
    // Attaches the body field by default.
337
    $instance = entity_create('field_instance', array(
338 339 340 341 342 343
      'field_name' => 'comment_body',
      'label' => 'Comment',
      'entity_type' => 'comment',
      'bundle' => 'comment_node_' . $info->type,
      'settings' => array('text_processing' => 1),
      'required' => TRUE,
344 345
    ));
    $instance->save();
346 347 348 349 350 351 352 353 354

    // 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.
355 356 357 358 359 360 361
    entity_get_display('comment', 'comment_node_' . $info->type, 'default')
      ->setComponent('comment_body', array(
        'label' => 'hidden',
        'type' => 'text_default',
        'weight' => 0,
      ))
      ->save();
362
  }
363 364
}

Dries's avatar
 
Dries committed
365
/**
366
 * Implements hook_permission().
Dries's avatar
 
Dries committed
367
 */
368
function comment_permission() {
369
  return array(
370
    'administer comments' => array(
371
      'title' => t('Administer comments and comment settings'),
372 373
    ),
    'access comments' => array(
374
      'title' => t('View comments'),
375 376
    ),
    'post comments' => array(
377
      'title' => t('Post comments'),
378
    ),
379 380
    'skip comment approval' => array(
      'title' => t('Skip comment approval'),
381
    ),
382 383 384
    'edit own comments' => array(
      'title' => t('Edit own comments'),
    ),
385
  );
Dries's avatar
 
Dries committed
386 387
}

388
/**
389
 * Finds the most recent comments that are available to the current user.
390 391
 *
 * @param integer $number
392
 *   (optional) The maximum number of comments to find. Defaults to 10.
393
 *
394
 * @return
395 396
 *   An array of comment objects or an empty array if there are no recent
 *   comments visible to the current user.
397 398
 */
function comment_get_recent($number = 10) {
399
  $query = db_select('comment', 'c');
400
  $query->innerJoin('node_field_data', 'n', 'n.nid = c.nid');
401
  $query->addTag('node_access');
402
  $query->addMetaData('base_table', 'comment');
403 404 405 406
  $comments = $query
    ->fields('c')
    ->condition('c.status', COMMENT_PUBLISHED)
    ->condition('n.status', NODE_PUBLISHED)
407 408 409
    // @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)
410
    ->orderBy('c.created', 'DESC')
411 412 413
    // Additionally order by cid to ensure that comments with the same timestamp
    // are returned in the exact order posted.
    ->orderBy('c.cid', 'DESC')
414 415 416 417 418
    ->range(0, $number)
    ->execute()
    ->fetchAll();

  return $comments ? $comments : array();
419 420
}

421
/**
422
 * Calculates the page number for the first new comment.
423 424 425 426 427
 *
 * @param $num_comments
 *   Number of comments.
 * @param $new_replies
 *   Number of new replies.
428
 * @param \Drupal\Core\Entity\EntityInterface $node
429
 *   The first new comment node.
430
 *
431 432
 * @return
 *   "page=X" if the page number is greater than zero; empty string otherwise.
433
 */
434
function comment_new_page_count($num_comments, $new_replies, EntityInterface $node) {
435 436
  $mode = variable_get('comment_default_mode_' . $node->getType(), COMMENT_MODE_THREADED);
  $comments_per_page = variable_get('comment_default_per_page_' . $node->getType(), 50);
437
  $pagenum = NULL;
438
  $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
439 440
  if ($num_comments <= $comments_per_page) {
    // Only one page of comments.
441
    $pageno = 0;
442
  }
443 444 445
  elseif ($flat) {
    // Flat comments.
    $count = $num_comments - $new_replies;
446
    $pageno = $count / $comments_per_page;
447
  }
448
  else {
449 450 451 452 453 454
    // 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'))
455
      ->condition('nid', $node->id())
456
      ->condition('status', COMMENT_PUBLISHED)
457 458
      ->orderBy('created', 'DESC')
      ->orderBy('cid', 'DESC')
459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
      ->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,
475
      ':nid' => $node->id(),
476
      ':thread' => $first_thread,
477
    ))->fetchField();
478

479
    $pageno = $count / $comments_per_page;
480
  }
481

482
  if ($pageno >= 1) {
483
    $pagenum = array('page' => intval($pageno));
484
  }
485

486 487 488
  return $pagenum;
}

489
/**
490
 * Returns HTML for a list of recent comments.
491 492 493
 *
 * @ingroup themeable
 */
494
function theme_comment_block($variables) {
495
  $items = array();
496
  $number = $variables['number'];
497
  foreach (comment_get_recent($number) as $comment) {
498
    $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>';
499
  }
500

501
  if ($items) {
502 503 504 505 506
    $item_list = array(
      '#theme' => 'item_list',
      '#items' => $items,
    );
    return drupal_render($item_list);
507
  }
508 509 510
  else {
    return t('No comments available.');
  }
511 512
}

Dries's avatar
 
Dries committed
513
/**
514
 * Implements hook_node_view().
Dries's avatar
 
Dries committed
515
 */
516
function comment_node_view(EntityInterface $node, EntityDisplay $display, $view_mode) {
Dries's avatar
 
Dries committed
517 518
  $links = array();

519
  if ($node->comment != COMMENT_NODE_HIDDEN) {
520
    if ($view_mode == 'rss') {
521 522 523
      // Add a comments RSS element which is a URL to the comments of this node.
      $node->rss_elements[] = array(
        'key' => 'comments',
524
        'value' => url('node/' . $node->id(), array('fragment' => 'comments', 'absolute' => TRUE))
525
      );
526
    }
527
    elseif ($view_mode == 'teaser') {
528 529 530
      // 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
531
      if (user_access('access comments')) {
532
        if (!empty($node->comment_count)) {
533
          $links['comment-comments'] = array(
534
            'title' => format_plural($node->comment_count, '1 comment', '@count comments'),
535
            'href' => 'node/' . $node->id(),
536
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
537 538
            'fragment' => 'comments',
            'html' => TRUE,
539
          );
540
          // Show a link to the first new comment.
541
          if ($new = comment_num_new($node->id())) {
542
            $links['comment-new-comments'] = array(
543
              'title' => format_plural($new, '1 new comment', '@count new comments'),
544
              'href' => 'node/' . $node->id(),
545
              'query' => comment_new_page_count($node->comment_count, $new, $node),
546
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
547 548
              'fragment' => 'new',
              'html' => TRUE,
549
            );
Dries's avatar
 
Dries committed
550 551
          }
        }
552 553
      }
      if ($node->comment == COMMENT_NODE_OPEN) {
554
        $comment_form_location = variable_get('comment_form_location_' . $node->getType(), COMMENT_FORM_BELOW);
555 556 557
        if (user_access('post comments')) {
          $links['comment-add'] = array(
            'title' => t('Add new comment'),
558
            'href' => 'node/' . $node->id(),
559 560 561
            'attributes' => array('title' => t('Add a new comment to this page.')),
            'fragment' => 'comment-form',
          );
562
          if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
563
            $links['comment-add']['href'] = 'comment/reply/' . $node->id();
564
          }
565
        }
Dries's avatar
 
Dries committed
566
        else {
567 568 569 570
          $comment_post_forbidden = array(
            '#theme' => 'comment_post_forbidden',
            '#node' => $node,
          );
571
          $links['comment-forbidden'] = array(
572
            'title' => drupal_render($comment_post_forbidden),
573 574
            'html' => TRUE,
          );
Dries's avatar
 
Dries committed
575 576 577
        }
      }
    }
578
    elseif ($view_mode != 'search_index' && $view_mode != 'search_result') {
579 580 581
      // 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
582
      // indexing or constructing a search result excerpt.
583
      if ($node->comment == COMMENT_NODE_OPEN) {
584
        $comment_form_location = variable_get('comment_form_location_' . $node->getType(), COMMENT_FORM_BELOW);
Dries's avatar
 
Dries committed
585
        if (user_access('post comments')) {
586 587 588 589 590 591
          // 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.')),
592
              'href' => 'node/' . $node->id(),
593 594 595
              'fragment' => 'comment-form',
            );
            if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
596
              $links['comment-add']['href'] = 'comment/reply/' . $node->id();
597
            }
598
          }
Dries's avatar
 
Dries committed
599 600
        }
        else {
601 602 603 604
          $comment_post_forbidden = array(
            '#theme' => 'comment_post_forbidden',
            '#node' => $node,
          );
605
          $links['comment-forbidden'] = array(
606
            'title' => drupal_render($comment_post_forbidden),
607 608
            'html' => TRUE,
          );
Dries's avatar
 
Dries committed
609 610 611
        }
      }
    }
Dries's avatar
Dries committed
612

613 614 615 616 617
    $node->content['links']['comment'] = array(
      '#theme' => 'links__node__comment',
      '#links' => $links,
      '#attributes' => array('class' => array('links', 'inline')),
    );
Dries's avatar
Dries committed
618

619 620 621
    // 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
622
    // displayed in 'full' view mode within another node.
623
    if ($node->comment && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) {
624
      $node->content['comments'] = comment_node_page_additions($node);
625
    }
626
  }
Dries's avatar
 
Dries committed
627 628
}

629
/**
630
 * Builds the comment-related elements for node detail pages.
631
 *
632
 * @param \Drupal\Core\Entity\EntityInterface $node
633
 *   The node entity for which to build the comment-related elements.
634 635 636 637
 *
 * @return
 *   A renderable array representing the comment-related page elements for the
 *   node.
638
 */
639
function comment_node_page_additions(EntityInterface $node) {
640 641 642 643 644
  $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.
645
  if (($node->comment_count && user_access('access comments')) || user_access('administer comments')) {
646 647
    $mode = variable_get('comment_default_mode_' . $node->getType(), COMMENT_MODE_THREADED);
    $comments_per_page = variable_get('comment_default_per_page_' . $node->getType(), 50);
648
    if ($cids = comment_get_thread($node, $mode, $comments_per_page)) {
649 650
      $comments = comment_load_multiple($cids);
      comment_prepare_thread($comments);
651
      $build = comment_view_multiple($comments);
652 653 654 655 656 657
      $build['pager']['#theme'] = 'pager';
      $additions['comments'] = $build;
    }
  }

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

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

  return $additions;
}

674 675 676
/**
 * Returns a rendered form to comment the given node.
 *
677
 * @param \Drupal\Core\Entity\EntityInterface $node
678
 *   The node entity to be commented.
679 680 681
 * @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.
682 683 684 685
 *
 * @return array
 *   The renderable array for the comment addition form.
 */
686
function comment_add(EntityInterface $node, $pid = NULL) {
687
  $values = array('nid' => $node->id(), 'pid' => $pid, 'node_type' => 'comment_node_' . $node->getType());
688
  $comment = entity_create('comment', $values);
689
  return Drupal::entityManager()->getForm($comment);
690 691
}

692
/**
693
 * Retrieves comments for a thread.
694
 *
695
 * @param \Drupal\Core\Entity\EntityInterface $node
696
 *   The node whose comment(s) needs rendering.
697 698 699 700
 * @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.
701
 *
702 703 704
 * @return
 *   An array of the IDs of the comment to be displayed.
 *
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 757 758
 * 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.
 */
759
function comment_get_thread(EntityInterface $node, $mode, $comments_per_page) {
760 761
  $query = db_select('comment', 'c')
    ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
762 763
  $query->addField('c', 'cid');
  $query
764
    ->condition('c.nid', $node->id())
765
    ->addTag('node_access')
766
    ->addTag('comment_filter')
767
    ->addMetaData('base_table', 'comment')
768
    ->addMetaData('node', $node)
769 770 771 772 773
    ->limit($comments_per_page);

  $count_query = db_select('comment', 'c');
  $count_query->addExpression('COUNT(*)');