comment.module 61.3 KB
Newer Older
1
<?php
2

Dries's avatar
Dries committed
3 4
/**
 * @file
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
use Drupal\entity\Plugin\Core\Entity\EntityDisplay;
14
use Drupal\file\Plugin\Core\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;
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;
40 41

/**
42
 * Anonymous posters cannot enter their contact information.
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;
55 56

/**
57
 * Comment form should be displayed on a separate page.
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;
65 66

/**
67
 * Comments for this node are hidden.
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\Plugin\Core\Entity\Comment;
82

83
/**
84
 * Implements hook_help().
85
 */
86 87
function comment_help($path, $arg) {
  switch ($path) {
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
  }
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
    ),
  );
}

181
/**
182
 * Implements hook_menu().
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
  );
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
}

365
/**
366
 * Implements hook_permission().
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
  );
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->type, COMMENT_MODE_THREADED);
  $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 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
}

513
/**
514
 * Implements hook_node_view().
515
 */
516
function comment_node_view(EntityInterface $node, EntityDisplay $display, $view_mode) {
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.
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
            );
550 551
          }
        }
552 553
      }
      if ($node->comment == COMMENT_NODE_OPEN) {
554
        $comment_form_location = variable_get('comment_form_location_' . $node->type, 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
        }
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,
          );
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->type, COMMENT_FORM_BELOW);
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
          }
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,
          );
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
  }
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 648
    $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)) {
649 650
      $comments = comment_load_multiple($cids);
      comment_prepare_thread($comments);
651
      $build = comment_view_multiple($comments);
652 653 654 655 656 657 658
      $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)) {
659
    $additions['comment_form'] = comment_add($node);
660 661 662 663
  }

  if ($additions) {
    $additions += array(
664
      '#theme' => 'comment_wrapper__node_' . $node->type,
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->type);
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(*)');
  $count_query
774
    ->condition('c.nid', $node->id())
775 776
    ->addTag('node_access')
    ->addTag('comment_filter')
777
    ->addMetaData('base_table', 'comment')
778
    ->addMetaData('node', $node);
779 780 781 782 783 784 785 786 787 788 789 790

  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.
791 792
    $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
    $query->orderBy('torder', 'ASC');
793 794 795 796 797 798 799 800 801
  }

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

  return $cids;
}

/**
802 803 804 805 806
 * 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.
807 808
 *
 * @param array $comments
809
 *   An array of comment objects, keyed by comment ID.
810 811 812 813 814 815 816 817 818
 */
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) {
819
    if ($first_new && $comment->new->value != MARK_READ) {
820 821 822 823 824 825 826 827
      // 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).
828
    $comment->depth = count(explode('.', $comment->thread->value)) - 1;
829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846
    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;
}

/**
847
 * Generates an array for rendering a comment.
848
 *
849
 * @param Drupal\comment\Comment $comment
850
 *   The comment object.
851 852
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
853 854 855
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
856 857 858 859
 *
 * @return
 *   An array as expected by drupal_render().
 */
860 861
function comment_view(Comment $comment, $view_mode = 'full', $langcode = NULL) {
  return entity_view($comment, $view_mode, $langcode);
862 863
}

864
/**
865
 * Adds reply, edit, delete, etc. links, depending on user permissions.
866
 *
867
 * @param Drupal\comment\Comment $comment
868
 *   The comment object.
869
 * @param \Drupal\Core\Entity\EntityInterface $node
870
 *   The node the comment is attached to.
871
 *
872 873 874
 * @return
 *   A structured array of links.
 */
875
function comment_links(Comment $comment, EntityInterface $node) {
876 877
  $links = array();
  if ($node->comment == COMMENT_NODE_OPEN) {
878
    if ($comment->access('delete')) {
879
      $links['comment-delete'] = array(
880
        'title' => t('delete'),
881
        'href' => "comment/{$comment->id()}/delete",
882 883 884
        'html' => TRUE,
      );
    }
885
    if ($comment->access('update')) {
886
        $links['comment-edit'] = array(
887
          'title' => t('edit'),
888
          'href' => "comment/{$comment->id()}/edit",
889 890
          'html' => TRUE,
        );
891 892
    }
    if ($comment->access('create')) {
893
      $links['comment-reply'] = array(
894
        'title' => t('reply'),
895
        'href' => "comment/reply/{$comment->nid->target_id}/{$comment->id()}",
896 897 898
        'html' => TRUE,
      );
    }
899 900 901 902 903 904 905 906 907
    if ($comment->status->value == COMMENT_NOT_PUBLISHED && $comment->access('approve')) {
      $links['comment-approve'] = array(
        'title' => t('approve'),
        'href' => "comment/{$comment->id()}/approve",
        'html' => TRUE,
        'query' => array('token' => drupal_get_token("comment/{$comment->id()}/approve")),
      );
    }
    if (empty($links)) {
908 909 910 911 912
      $comment_post_forbidden = array(
        '#theme' => 'comment_post_forbidden',
        '#node' => $node,
      );
      $links['comment-forbidden']['title'] = drupal_render($comment_post_forbidden);
913
      $links['comment-forbidden']['html'] = TRUE;
914 915
    }
  }
916 917

  // Add translations link for translation-enabled comment bundles.
918
  if (module_exists('content_translation') && content_translation_translate_access($comment)) {
919
    $links['comment-translations'] = array(
920
      'title' => t('translate'),
921 922 923 924 925
      'href' => 'comment/' . $comment->id() . '/translations',
      'html' => TRUE,
    );
  }

926 927 928
  return $links;
}

929
/**
930
 * Constructs render array from an array of loaded comments.
931 932 933
 *
 * @param $comments
 *   An array of comments as returned by comment_load_multiple().
934 935
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
936 937 938 939
 * @param $langcode
 *   A string indicating the language field values are to be shown in. If no
 *   language is provided the current content language is used.
 *
940 941
 * @return
 *   An array in the format expected by drupal_render().
942 943
 *
 * @see drupal_render()
944
 */
945 946
function comment_view_multiple($comments, $view_mode = 'full', $langcode = NULL) {
  return entity_view_multiple($comments, $view_mode, $langcode);
947 948
}

949
/**
950
 * Implements hook_form_FORM_ID_alter().
951
 */
952
function comment_form_node_type_form_alter(&$form, $form_state) {
953
  if (isset($form['type'])) {
954
    $node_type = $form_state['controller']->getEntity();
955
    $form['comment'] = array(
956
      '#type' => 'details',
957
      '#title' => t('Comment settings'),
958
      '#collapsed' => TRUE,
959
      '#group' => 'additional_settings',
960 961 962
      '#attributes' => array(
        'class' => array('comment-node-type-settings-form'),
      ),
963
      '#attached' => array(
964
        'library' => array(array('comment', 'drupal.comment')),
965
      ),
966
    );
967 968 969
    // Unlike coment_form_node_form_alter(), all of these settings are applied
    // as defaults to all new nodes. Therefore, it would be wrong to use #states
    // to hide the other settings based on the primary comment setting.
970 971 972
    $form['comment']['comment'] = array(
      '#type' => 'select',
      '#title' => t('Default comment setting for new content'),
973
      '#default_value' => variable_get('comment_' . $node_type->id(), COMMENT_NODE_OPEN),
974 975 976 977 978
      '#options' => array(
        COMMENT_NODE_OPEN => t('Open'),
        COMMENT_NODE_CLOSED => t('Closed'),
        COMMENT_NODE_HIDDEN => t('Hidden'),
      ),
979
    );
980
    $form['comment']['comment_default_mode'] = array(
981 982
      '#type' => 'checkbox',
      '#title' => t('Threading'),
983
      '#default_value' => variable_get('comment_default_mode_' . $node_type->id(), COMMENT_MODE_THREADED),
984
      '#description' => t('Show comment replies in a threaded list.'),
985 986 987
    );
    $form['comment']['comment_default_per_page'] = array(
      '#type' => 'select',
988
      '#title' => t('Comments per page'),
989
      '#default_value' => variable_get('comment_default_per_page_' . $node_type->id(), 50),
990
      '#options' => _comment_per_page(),
991
    );
992
    $form['comment']['comment_anonymous'] = array(
993
      '#type' => 'select',
994
      '#title' => t('Anonymous commenting'),
995
      '#default_value' => variable_get('comment_anonymous_' . $node_type->id(), COMMENT_ANONYMOUS_MAYNOT_CONTACT),
996 997 998
      '#options' => array(
        COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'),
        COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'),
999 1000 1001
        COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information'),
      ),
      '#access' => user_access('post comments', drupal_anonymous_user()),
1002 1003
    );
    $form['comment']['comment_subject_field'] = array(
1004 1005
      '#type' => 'checkbox',
      '#title' => t('Allow comment title'),
1006
      '#default_value' => variable_get('comment_subject_field_' . $node_type->id(), 1),
1007 1008
    );
    $form['comment']['comment_form_location'] = array(
1009 1010
      '#type' => 'checkbox',
      '#title' => t('Show reply form on the same page as comments'),
1011
      '#default_value' => variable_get('comment_form_location_' . $node_type->id(), COMMENT_FORM_BELOW),
1012 1013
    );
    $form['comment']['comment_preview'] = array(
1014 1015
      '#type' => 'radios',
      '#title' => t('Preview comment'),
1016
      '#default_value' => variable_get('comment_preview_' . $node_type->id(), DRUPAL_OPTIONAL),
1017 1018 1019 1020 1021
      '#options' => array(
        DRUPAL_DISABLED => t('Disabled'),
        DRUPAL_OPTIONAL => t('Optional'),
        DRUPAL_REQUIRED => t('Required'),
      ),
1022
    );
1023
    // @todo Remove this check once language settings are generalized.
1024
    if (module_exists('content_translation')) {
1025
      $comment_form = $form;
1026
      $comment_form_state['content_translation']['key'] = 'language_configuration';
1027
      $form['comment'] += content_translation_enable_widget('comment', 'comment_node_' . $node_type->id(), $comment_form, $comment_form_state);
1028 1029
      array_unshift($form['#submit'], 'comment_translation_configuration_element_submit');
    }
1030
  }
1031 1032
}

1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047
/**
 * Form submission handler for node_type_form().
 *
 * This handles the comment translation settings added by
 * comment_form_node_type_form_alter().
 *
 * @see comment_form_node_type_form_alter()
 */
function comment_translation_configuration_element_submit($form, &$form_state) {
  // The comment translation settings form element is embedded into the node
  // type form. Hence we need to provide to the regular submit handler a
  // manipulated form state to make it process comment settings instead of node
  // settings.
  $key = 'language_configuration';
  $comment_form_state = array(
1048
    'content_translation' => array('key' => $key),
1049
    'language' => array($key => array('entity_type' => 'comment', 'bundle' => 'comment_node_' . $form_state['controller']->getEntity()->id())),
1050
    'values' => array($key => array('content_translation' => $form_state['values']['content_translation'])),
1051
  );
1052
  content_translation_language_configuration_element_submit($form, $comment_form_state);
1053 1054
}

1055
/**
1056
 * Implements hook_form_BASE_FORM_ID_alter().
1057
 */
1058
function comment_form_node_form_alter(&$form, $form_state) {
1059
  $node = $form_state['controller']->getEntity();
1060
  $form['comment_settings'] = array(
1061
    '#type' => 'details',
1062 1063
    '#access' => user_access('administer comments'),
    '#title' => t('Comment settings'),
1064
    '#collapsed' => TRUE,
1065
    '#group' => 'advanced',
1066 1067 1068
    '#attributes' => array(
      'class' => array('comment-node-settings-form'),
    ),
1069
    '#attached' => array(
1070
      'library' => array(array('comment', 'drupal.comment')),
1071
    ),
1072 1073
    '#weight' => 30,
  );
1074
  $comment_count = $node->id() ? db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array(':nid' => $node->id()))->fetchField() : 0;
1075 1076 1077
  $comment_settings = ($node->comment == COMMENT_NODE_HIDDEN && empty($comment_count)) ? COMMENT_NODE_CLOSED : $node->comment;
  $form['comment_settings']['comment'] = array(
    '#type' => 'radios',
1078 1079
    '#title' => t('Comments'),
    '#title_display' => 'invisible',
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099
    '#parents' => array('comment'),
    '#default_value' => $comment_settings,
    '#options' => array(
      COMMENT_NODE_OPEN => t('Open'),
      COMMENT_NODE_CLOSED => t('Closed'),
      COMMENT_NODE_HIDDEN => t('Hidden'),
    ),
    COMMENT_NODE_OPEN => array(
      '#description' => t('Users with the "Post comments" permission can post comments.'),
    ),
    COMMENT_NODE_CLOSED => array(
      '#description' => t('Users cannot post comments, but existing comments will be displayed.'),
    ),
    COMMENT_NODE_HIDDEN => array(
      '#description' => t('Comments are hidden from view.'),
    ),
  );
  // If the node doesn't have any comments, the "hidden" option makes no
  // sense, so don't even bother presenting it to the user.
  if (empty($comment_count)) {
1100 1101
    $form['comment_settings']['comment'][COMMENT_NODE_HIDDEN]['#access'] = FALSE;
    // Also adjust the description of the "closed" option.