comment.module 82.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 13
use Drupal\node\Node;

14
/**
15
 * Comment is awaiting approval.
16
 */
17
const COMMENT_NOT_PUBLISHED = 0;
18 19

/**
20
 * Comment is published.
21
 */
22
const COMMENT_PUBLISHED = 1;
Dries's avatar
Dries committed
23

24 25 26
/**
 * Comments are displayed in a flat list - expanded.
 */
27
const COMMENT_MODE_FLAT = 0;
28 29 30 31

/**
 * Comments are displayed as a threaded list - expanded.
 */
32
const COMMENT_MODE_THREADED = 1;
Dries's avatar
Dries committed
33 34

/**
35
 * Anonymous posters cannot enter their contact information.
Dries's avatar
Dries committed
36
 */
37
const COMMENT_ANONYMOUS_MAYNOT_CONTACT = 0;
38 39 40 41

/**
 * Anonymous posters may leave their contact information.
 */
42
const COMMENT_ANONYMOUS_MAY_CONTACT = 1;
43 44

/**
45
 * Anonymous posters are required to leave their contact information.
46
 */
47
const COMMENT_ANONYMOUS_MUST_CONTACT = 2;
Dries's avatar
Dries committed
48 49

/**
50
 * Comment form should be displayed on a separate page.
Dries's avatar
Dries committed
51
 */
52
const COMMENT_FORM_SEPARATE_PAGE = 0;
53 54 55 56

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

/**
60
 * Comments for this node are hidden.
Dries's avatar
Dries committed
61
 */
62
const COMMENT_NODE_HIDDEN = 0;
63 64

/**
65
 * Comments for this node are closed.
66
 */
67
const COMMENT_NODE_CLOSED = 1;
68 69

/**
70
 * Comments for this node are open.
71
 */
72
const COMMENT_NODE_OPEN = 2;
73

74
/**
75
 * Implements hook_help().
76
 */
77 78
function comment_help($path, $arg) {
  switch ($path) {
Dries's avatar
 
Dries committed
79
    case 'admin/help#comment':
80
      $output = '<h3>' . t('About') . '</h3>';
81
      $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>';
82 83 84
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Default and custom settings') . '</dt>';
85
      $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>';
86 87
      $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>';
88
      $output .= '</dl>';
89
      return $output;
90
  }
Dries's avatar
 
Dries committed
91 92
}

93
/**
94
 * Implements hook_entity_info().
95 96
 */
function comment_entity_info() {
97
  $return = array(
98 99 100
    'comment' => array(
      'label' => t('Comment'),
      'base table' => 'comment',
101
      'uri callback' => 'comment_uri',
102
      'fieldable' => TRUE,
103 104
      'controller class' => 'CommentStorageController',
      'entity class' => 'Comment',
105
      'entity keys' => array(
106 107
        'id' => 'cid',
        'bundle' => 'node_type',
108
        'label' => 'subject',
109 110
      ),
      'bundles' => array(),
111 112 113
      'view modes' => array(
        'full' => array(
          'label' => t('Full comment'),
114
          'custom settings' => FALSE,
115 116
        ),
      ),
117 118 119 120 121 122
      'static cache' => FALSE,
    ),
  );

  foreach (node_type_get_names() as $type => $name) {
    $return['comment']['bundles']['comment_node_' . $type] = array(
123
      'label' => t('@node_type comment', array('@node_type' => $name)),
124 125 126
      // 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,
127 128 129 130 131 132
      'admin' => array(
        // Place the Field UI paths for comments one level below the
        // corresponding paths for nodes, so that they appear in the same set
        // of local tasks. Note that the paths use a different placeholder name
        // and thus a different menu loader callback, so that Field UI page
        // callbacks get a comment bundle name from the node type in the URL.
133
        // See comment_node_type_load() and comment_menu_alter().
134 135 136 137 138
        'path' => 'admin/structure/types/manage/%comment_node_type/comment',
        'bundle argument' => 4,
        'real path' => 'admin/structure/types/manage/' . str_replace('_', '-', $type) . '/comment',
        'access arguments' => array('administer content types'),
      ),
139 140 141 142 143 144
    );
  }

  return $return;
}

145
/**
146
 * Loads the comment bundle name corresponding a given content type.
147
 *
148 149 150 151 152 153 154 155 156 157 158
 * This function is used as a menu loader callback in comment_menu().
 *
 * @param $name
 *   The URL-formatted machine name of the node type whose comment fields are
 *   to be edited. 'URL-formatted' means that underscores are replaced by
 *   hyphens.
 *
 * @return
 *   The comment bundle name corresponding to the node type.
 *
 * @see comment_menu_alter()
159 160 161 162 163 164 165
 */
function comment_node_type_load($name) {
  if ($type = node_type_get_type(strtr($name, array('-' => '_')))) {
    return 'comment_node_' . $type->type;
  }
}

166
/**
167
 * Entity uri callback.
168
 */
169
function comment_uri(Comment $comment) {
170 171 172 173
  return array(
    'path' => 'comment/' . $comment->cid,
    'options' => array('fragment' => 'comment-' . $comment->cid),
  );
174 175
}

176 177 178 179 180 181 182 183 184
/**
 * 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(
185 186 187 188 189 190
        'form' => array(
          'author' => array(
            'label' => t('Author'),
            'description' => t('Author textfield'),
            'weight' => -2,
          ),
191
          'subject' => array(
192 193 194 195
            'label' => t('Subject'),
            'description' => t('Subject textfield'),
            'weight' => -1,
          ),
196 197 198 199 200 201 202 203
        ),
      );
    }
  }

  return $return;
}

204
/**
205
 * Implements hook_theme().
206 207 208 209
 */
function comment_theme() {
  return array(
    'comment_block' => array(
210
      'variables' => array(),
211 212
    ),
    'comment_preview' => array(
213
      'variables' => array('comment' => NULL),
214 215
    ),
    'comment' => array(
216
      'template' => 'comment',
217
      'render element' => 'elements',
218 219
    ),
    'comment_post_forbidden' => array(
220
      'variables' => array('node' => NULL),
221 222
    ),
    'comment_wrapper' => array(
223
      'template' => 'comment-wrapper',
224
      'render element' => 'content',
225 226 227 228
    ),
  );
}

Dries's avatar
 
Dries committed
229
/**
230
 * Implements hook_menu().
Dries's avatar
 
Dries committed
231
 */
232
function comment_menu() {
233
  $items['admin/content/comment'] = array(
234
    'title' => 'Comments',
235
    'description' => 'List and edit site comments and the comment approval queue.',
236 237
    'page callback' => 'comment_admin',
    'access arguments' => array('administer comments'),
238
    'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM,
239
    'file' => 'comment.admin.inc',
240
  );
241
  // Tabs begin here.
242
  $items['admin/content/comment/new'] = array(
243
    'title' => 'Published comments',
244 245 246
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
247
  $items['admin/content/comment/approval'] = array(
248 249
    'title' => 'Unapproved comments',
    'title callback' => 'comment_count_unpublished',
250
    'page arguments' => array('approval'),
251
    'access arguments' => array('administer comments'),
252 253
    'type' => MENU_LOCAL_TASK,
  );
254
  $items['comment/%'] = array(
255 256 257 258
    'title' => 'Comment permalink',
    'page callback' => 'comment_permalink',
    'page arguments' => array(1),
    'access arguments' => array('access comments'),
259
  );
260
  $items['comment/%/view'] = array(
261 262 263 264
    'title' => 'View comment',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
265 266
  // 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).
267 268
  $items['comment/%comment/edit'] = array(
    'title' => 'Edit',
269 270
    'page callback' => 'comment_edit_page',
    'page arguments' => array(1),
271
    'access callback' => 'comment_access',
272 273 274 275
    'access arguments' => array('edit', 1),
    'type' => MENU_LOCAL_TASK,
    'weight' => 0,
  );
276
  $items['comment/%/approve'] = array(
277 278 279 280 281 282 283
    'title' => 'Approve',
    'page callback' => 'comment_approve',
    'page arguments' => array(1),
    'access arguments' => array('administer comments'),
    'file' => 'comment.pages.inc',
    'weight' => 1,
  );
284
  $items['comment/%/delete'] = array(
285
    'title' => 'Delete',
286 287
    'page callback' => 'comment_confirm_delete_page',
    'page arguments' => array(1),
288 289 290 291
    'access arguments' => array('administer comments'),
    'type' => MENU_LOCAL_TASK,
    'file' => 'comment.admin.inc',
    'weight' => 2,
292
  );
293
  $items['comment/reply/%node'] = array(
294
    'title' => 'Add new comment',
295
    'page callback' => 'comment_reply',
296
    'page arguments' => array(2),
297 298
    'access callback' => 'node_access',
    'access arguments' => array('view', 2),
299
    'file' => 'comment.pages.inc',
300
  );
Dries's avatar
 
Dries committed
301 302 303 304

  return $items;
}

305 306 307 308 309
/**
 * Implements hook_menu_alter().
 */
function comment_menu_alter(&$items) {
  // Add comments to the description for admin/content.
310
  $items['admin/content']['description'] = 'Administer content and comments.';
311 312 313 314 315 316 317 318 319

  // Adjust the Field UI tabs on admin/structure/types/manage/[node-type].
  // See comment_entity_info().
  $items['admin/structure/types/manage/%comment_node_type/comment/fields']['title'] = 'Comment fields';
  $items['admin/structure/types/manage/%comment_node_type/comment/fields']['weight'] = 3;
  $items['admin/structure/types/manage/%comment_node_type/comment/display']['title'] = 'Comment display';
  $items['admin/structure/types/manage/%comment_node_type/comment/display']['weight'] = 4;
}

320 321 322 323 324 325 326 327 328 329
/**
 * 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));
}

330
/**
331
 * Implements hook_node_type_insert().
332
 *
333 334
 * 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,
335 336 337
 * hook_modules_enabled() serves to create the body fields.
 *
 * @see comment_modules_enabled()
338
 */
339
function comment_node_type_insert($info) {
340
  _comment_body_field_create($info);
341
}
342

343
/**
344
 * Implements hook_node_type_update().
345 346 347
 */
function comment_node_type_update($info) {
  if (!empty($info->old_type) && $info->type != $info->old_type) {
348
    field_attach_rename_bundle('comment', 'comment_node_' . $info->old_type, 'comment_node_' . $info->type);
349 350
  }
}
351

352
/**
353
 * Implements hook_node_type_delete().
354 355
 */
function comment_node_type_delete($info) {
356
  field_attach_delete_bundle('comment', 'comment_node_' . $info->type);
357 358 359 360 361 362 363 364 365 366 367
  $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);
368 369 370
  }
}

371
 /**
372
 * Creates a comment_body field instance for a given node type.
373 374 375 376 377
 *
 * @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.
378
 */
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
function _comment_body_field_create($info) {
  // Create the field if needed.
  if (!field_read_field('comment_body', array('include_inactive' => TRUE))) {
    $field = array(
      'field_name' => 'comment_body',
      'type' => 'text_long',
      'entity_types' => array('comment'),
    );
    field_create_field($field);
  }
  // Create the instance if needed.
  if (!field_read_instance('comment', 'comment_body', 'comment_node_' . $info->type, array('include_inactive' => TRUE))) {
    field_attach_create_bundle('comment', 'comment_node_' . $info->type);
    // Attaches the body field by default.
    $instance = array(
      'field_name' => 'comment_body',
      'label' => 'Comment',
      'entity_type' => 'comment',
      'bundle' => 'comment_node_' . $info->type,
      'settings' => array('text_processing' => 1),
      'required' => TRUE,
      'display' => array(
        'default' => array(
          'label' => 'hidden',
          'type' => 'text_default',
          'weight' => 0,
        ),
406
      ),
407 408 409
    );
    field_create_instance($instance);
  }
410 411
}

Dries's avatar
 
Dries committed
412
/**
413
 * Implements hook_permission().
Dries's avatar
 
Dries committed
414
 */
415
function comment_permission() {
416
  return array(
417
    'administer comments' => array(
418
      'title' => t('Administer comments and comment settings'),
419 420
    ),
    'access comments' => array(
421
      'title' => t('View comments'),
422 423
    ),
    'post comments' => array(
424
      'title' => t('Post comments'),
425
    ),
426 427
    'skip comment approval' => array(
      'title' => t('Skip comment approval'),
428
    ),
429 430 431
    'edit own comments' => array(
      'title' => t('Edit own comments'),
    ),
432
  );
Dries's avatar
 
Dries committed
433 434 435
}

/**
436
 * Implements hook_block_info().
Dries's avatar
 
Dries committed
437
 */
438
function comment_block_info() {
439
  $blocks['recent']['info'] = t('Recent comments');
440
  $blocks['recent']['properties']['administrative'] = TRUE;
441

442 443
  return $blocks;
}
444

445
/**
446
 * Implements hook_block_configure().
447 448 449 450 451 452 453 454
 */
function comment_block_configure($delta = '') {
  $form['comment_block_count'] = array(
    '#type' => 'select',
    '#title' => t('Number of recent comments'),
    '#default_value' => variable_get('comment_block_count', 10),
    '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30)),
  );
455

456 457
  return $form;
}
458

459
/**
460
 * Implements hook_block_save().
461 462
 */
function comment_block_save($delta = '', $edit = array()) {
463
  variable_set('comment_block_count', (int) $edit['comment_block_count']);
464
}
465

466
/**
467
 * Implements hook_block_view().
468 469 470 471 472 473 474
 *
 * Generates a block with the most recent comments.
 */
function comment_block_view($delta = '') {
  if (user_access('access comments')) {
    $block['subject'] = t('Recent comments');
    $block['content'] = theme('comment_block');
475

476
    return $block;
Dries's avatar
 
Dries committed
477 478 479
  }
}

480 481 482 483 484 485 486 487 488 489 490
/**
 * Redirects comment links to the correct page depending on comment settings.
 *
 * Since comments are paged there is no way to guarantee which page a comment
 * appears on. Comment paging and threading settings may be changed at any time.
 * With threaded comments, an individual comment may move between pages as
 * comments can be added either before or after it in the overall discussion.
 * Therefore we use a central routing function for comment links, which
 * calculates the page number based on current comment settings and returns
 * the full comment view with the pager set dynamically.
 *
491 492
 * @param $cid
 *   A comment identifier.
493
 *
494 495 496
 * @return
 *   The comment listing set to the page on which the comment appears.
 */
497 498
function comment_permalink($cid) {
  if (($comment = comment_load($cid)) && ($node = node_load($comment->nid))) {
499 500 501 502 503

    // Find the current display page for this comment.
    $page = comment_get_display_page($comment->cid, $node->type);

    // Return the node view, this will show the correct comment in context.
504 505 506
    // @todo Refactor to use Symfony's Request object.
    _current_path('node/' . $node->nid);
    $_GET['page'] = $page;
507
    return menu_execute_active_handler('node/' . $node->nid, FALSE);
508 509 510 511
  }
  drupal_not_found();
}

512
/**
513
 * Finds the most recent comments that are available to the current user.
514 515
 *
 * @param integer $number
516
 *   (optional) The maximum number of comments to find. Defaults to 10.
517
 *
518
 * @return
519 520
 *   An array of comment objects or an empty array if there are no recent
 *   comments visible to the current user.
521 522
 */
function comment_get_recent($number = 10) {
523 524 525 526 527 528 529
  $query = db_select('comment', 'c');
  $query->innerJoin('node', 'n', 'n.nid = c.nid');
  $query->addTag('node_access');
  $comments = $query
    ->fields('c')
    ->condition('c.status', COMMENT_PUBLISHED)
    ->condition('n.status', NODE_PUBLISHED)
530
    ->orderBy('c.created', 'DESC')
531 532 533
    // Additionally order by cid to ensure that comments with the same timestamp
    // are returned in the exact order posted.
    ->orderBy('c.cid', 'DESC')
534 535 536 537 538
    ->range(0, $number)
    ->execute()
    ->fetchAll();

  return $comments ? $comments : array();
539 540
}

541
/**
542
 * Calculates the page number for the first new comment.
543 544 545 546 547
 *
 * @param $num_comments
 *   Number of comments.
 * @param $new_replies
 *   Number of new replies.
548
 * @param Drupal\node\Node $node
549
 *   The first new comment node.
550
 *
551 552
 * @return
 *   "page=X" if the page number is greater than zero; empty string otherwise.
553
 */
554
function comment_new_page_count($num_comments, $new_replies, Node $node) {
555 556
  $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
  $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
557
  $pagenum = NULL;
558
  $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
559 560
  if ($num_comments <= $comments_per_page) {
    // Only one page of comments.
561
    $pageno = 0;
562
  }
563 564 565
  elseif ($flat) {
    // Flat comments.
    $count = $num_comments - $new_replies;
566
    $pageno = $count / $comments_per_page;
567
  }
568
  else {
569 570 571 572 573 574 575 576
    // 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'))
      ->condition('nid', $node->nid)
      ->condition('status', COMMENT_PUBLISHED)
577 578
      ->orderBy('created', 'DESC')
      ->orderBy('cid', 'DESC')
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
      ->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,
595
      ':nid' => $node->nid,
596
      ':thread' => $first_thread,
597
    ))->fetchField();
598

599
    $pageno = $count / $comments_per_page;
600
  }
601

602
  if ($pageno >= 1) {
603
    $pagenum = array('page' => intval($pageno));
604
  }
605

606 607 608
  return $pagenum;
}

609
/**
610
 * Returns HTML for a list of recent comments.
611 612 613
 *
 * @ingroup themeable
 */
614 615
function theme_comment_block() {
  $items = array();
616 617
  $number = variable_get('comment_block_count', 10);
  foreach (comment_get_recent($number) as $comment) {
618
    $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>';
619
  }
620

621
  if ($items) {
622
    return theme('item_list', array('items' => $items));
623
  }
624 625 626
  else {
    return t('No comments available.');
  }
627 628
}

Dries's avatar
 
Dries committed
629
/**
630
 * Implements hook_node_view().
Dries's avatar
 
Dries committed
631
 */
632
function comment_node_view(Node $node, $view_mode) {
Dries's avatar
 
Dries committed
633 634
  $links = array();

635
  if ($node->comment != COMMENT_NODE_HIDDEN) {
636
    if ($view_mode == 'rss') {
637 638 639 640 641
      // Add a comments RSS element which is a URL to the comments of this node.
      $node->rss_elements[] = array(
        'key' => 'comments',
        'value' => url('node/' . $node->nid, array('fragment' => 'comments', 'absolute' => TRUE))
      );
642
    }
643
    elseif ($view_mode == 'teaser') {
644 645 646
      // 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
647
      if (user_access('access comments')) {
648
        if (!empty($node->comment_count)) {
649
          $links['comment-comments'] = array(
650
            'title' => format_plural($node->comment_count, '1 comment', '@count comments'),
651 652
            'href' => "node/$node->nid",
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
653 654
            'fragment' => 'comments',
            'html' => TRUE,
655
          );
656 657
          // Show a link to the first new comment.
          if ($new = comment_num_new($node->nid)) {
658
            $links['comment-new-comments'] = array(
659
              'title' => format_plural($new, '1 new comment', '@count new comments'),
660
              'href' => "node/$node->nid",
661
              'query' => comment_new_page_count($node->comment_count, $new, $node),
662
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
663 664
              'fragment' => 'new',
              'html' => TRUE,
665
            );
Dries's avatar
 
Dries committed
666 667
          }
        }
668 669
      }
      if ($node->comment == COMMENT_NODE_OPEN) {
670
        $comment_form_location = variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW);
671 672 673
        if (user_access('post comments')) {
          $links['comment-add'] = array(
            'title' => t('Add new comment'),
674
            'href' => "node/$node->nid",
675 676 677
            'attributes' => array('title' => t('Add a new comment to this page.')),
            'fragment' => 'comment-form',
          );
678 679 680
          if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
            $links['comment-add']['href'] = "comment/reply/$node->nid";
          }
681
        }
Dries's avatar
 
Dries committed
682
        else {
683
          $links['comment-forbidden'] = array(
684 685 686
            'title' => theme('comment_post_forbidden', array('node' => $node)),
            'html' => TRUE,
          );
Dries's avatar
 
Dries committed
687 688 689
        }
      }
    }
690
    elseif ($view_mode != 'search_index' && $view_mode != 'search_result') {
691 692 693
      // 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
694
      // indexing or constructing a search result excerpt.
695
      if ($node->comment == COMMENT_NODE_OPEN) {
696
        $comment_form_location = variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW);
Dries's avatar
 
Dries committed
697
        if (user_access('post comments')) {
698 699 700 701 702 703 704 705 706 707 708 709
          // 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.')),
              'href' => "node/$node->nid",
              'fragment' => 'comment-form',
            );
            if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
              $links['comment-add']['href'] = "comment/reply/$node->nid";
            }
710
          }
Dries's avatar
 
Dries committed
711 712
        }
        else {
713
          $links['comment-forbidden'] = array(
714 715 716
            'title' => theme('comment_post_forbidden', array('node' => $node)),
            'html' => TRUE,
          );
Dries's avatar
 
Dries committed
717 718 719
        }
      }
    }
Dries's avatar
Dries committed
720

721 722 723 724 725
    $node->content['links']['comment'] = array(
      '#theme' => 'links__node__comment',
      '#links' => $links,
      '#attributes' => array('class' => array('links', 'inline')),
    );
Dries's avatar
Dries committed
726

727 728 729
    // 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
730
    // displayed in 'full' view mode within another node.
731
    if ($node->comment && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) {
732
      $node->content['comments'] = comment_node_page_additions($node);
733
    }
734
  }
Dries's avatar
 
Dries committed
735 736
}

737
/**
738
 * Builds the comment-related elements for node detail pages.
739
 *
740 741
 * @param Drupal\node\Node $node
 *   The node entity for which to build the comment-related elements.
742 743 744 745
 *
 * @return
 *   A renderable array representing the comment-related page elements for the
 *   node.
746
 */
747
function comment_node_page_additions(Node $node) {
748 749 750 751 752
  $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.
753
  if (($node->comment_count && user_access('access comments')) || user_access('administer comments')) {
754 755 756
    $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
    $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
    if ($cids = comment_get_thread($node, $mode, $comments_per_page)) {
757 758
      $comments = comment_load_multiple($cids);
      comment_prepare_thread($comments);
759
      $build = comment_view_multiple($comments, $node);
760 761 762 763 764 765 766
      $build['pager']['#theme'] = 'pager';
      $additions['comments'] = $build;
    }
  }

  // Append comment form if needed.
  if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) {
767 768
    $comment = entity_create('comment', array('nid' => $node->nid));
    $additions['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment);
769 770 771 772
  }

  if ($additions) {
    $additions += array(
773
      '#theme' => 'comment_wrapper__node_' . $node->type,
774 775 776 777 778 779 780 781 782 783
      '#node' => $node,
      'comments' => array(),
      'comment_form' => array(),
    );
  }

  return $additions;
}

/**
784
 * Retrieves comments for a thread.
785
 *
786
 * @param Drupal\node\Node $node
787
 *   The node whose comment(s) needs rendering.
788 789 790 791
 * @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.
792
 *
793 794 795
 * @return
 *   An array of the IDs of the comment to be displayed.
 *
796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849
 * 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.
 */
850
function comment_get_thread(Node $node, $mode, $comments_per_page) {
851 852 853 854 855
  $query = db_select('comment', 'c')->extend('PagerDefault');
  $query->addField('c', 'cid');
  $query
    ->condition('c.nid', $node->nid)
    ->addTag('node_access')
856 857
    ->addTag('comment_filter')
    ->addMetaData('node', $node)
858 859 860 861 862 863
    ->limit($comments_per_page);

  $count_query = db_select('comment', 'c');
  $count_query->addExpression('COUNT(*)');
  $count_query
    ->condition('c.nid', $node->nid)
864 865 866
    ->addTag('node_access')
    ->addTag('comment_filter')
    ->addMetaData('node', $node);
867 868 869 870 871 872 873 874 875 876 877 878

  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.
879 880
    $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
    $query->orderBy('torder', 'ASC');
881 882 883 884 885 886 887 888 889
  }

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

  return $cids;
}

/**
890 891 892 893 894
 * 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.
895 896
 *
 * @param array $comments
897
 *   An array of comment objects, keyed by comment ID.
898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934
 */
function comment_prepare_thread(&$comments) {
  // A flag stating if we are still searching for first new comment on the thread.
  $first_new = TRUE;

  // A counter that helps track how indented we are.
  $divs = 0;

  foreach ($comments as $key => $comment) {
    if ($first_new && $comment->new != MARK_READ) {
      // Assign the anchor only for the first new comment. This avoids duplicate
      // id attributes on a page.
      $first_new = FALSE;
      $comment->first_new = TRUE;
    }

    // The $divs element instructs #prefix whether to add an indent div or
    // close existing divs (a negative value).
    $comment->depth = count(explode('.', $comment->thread)) - 1;
    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;
}

/**
935
 * Generates an array for rendering a comment.
936
 *
937
 * @param Comment $comment
938
 *   The comment object.
939
 * @param Drupal\node\Node $node
940
 *   The node the comment is attached to.
941 942
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
943 944 945
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
946 947 948 949
 *
 * @return
 *   An array as expected by drupal_render().
 */
950
function comment_view(Comment $comment, Node $node, $view_mode = 'full', $langcode = NULL) {
951
  if (!isset($langcode)) {