comment.module 68.6 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\entity\Plugin\Core\Entity\EntityDisplay;
13
use Drupal\file\Plugin\Core\Entity\File;
14
use Drupal\Core\Entity\EntityInterface;
15
use Symfony\Component\HttpFoundation\Request;
16
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
17
use Symfony\Component\HttpKernel\HttpKernelInterface;
18

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

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

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

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

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

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

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

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

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

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

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

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

79
use Drupal\comment\Plugin\Core\Entity\Comment;
80

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

100 101 102 103 104
/**
 * Implements hook_entity_bundle_info().
 */
function comment_entity_bundle_info() {
  $bundles = array();
105
  foreach (node_type_get_names() as $type => $name) {
106
    $bundles['comment']['comment_node_' . $type] = array(
107
      'label' => t('@node_type comment', array('@node_type' => $name)),
108 109 110
      // 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,
111 112
    );
  }
113
  return $bundles;
114 115
}

116
/**
117
 * Loads the comment bundle name corresponding a given content type.
118
 *
119 120 121
 * This function is used as a menu loader callback in comment_menu().
 *
 * @param $name
122
 *   The machine name of the node type whose comment fields are to be edited.
123 124 125 126 127
 *
 * @return
 *   The comment bundle name corresponding to the node type.
 *
 * @see comment_menu_alter()
128 129
 */
function comment_node_type_load($name) {
130
  if ($type = node_type_load($name)) {
131 132 133 134
    return 'comment_node_' . $type->type;
  }
}

135
/**
136
 * Entity URI callback.
137
 */
138
function comment_uri(Comment $comment) {
139
  return array(
140 141
    'path' => 'comment/' . $comment->id(),
    'options' => array('fragment' => 'comment-' . $comment->id()),
142
  );
143 144
}

145 146 147 148 149 150 151 152 153
/**
 * 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(
154 155 156 157 158 159
        'form' => array(
          'author' => array(
            'label' => t('Author'),
            'description' => t('Author textfield'),
            'weight' => -2,
          ),
160
          'subject' => array(
161 162 163 164
            'label' => t('Subject'),
            'description' => t('Subject textfield'),
            'weight' => -1,
          ),
165 166 167 168 169 170 171 172
        ),
      );
    }
  }

  return $return;
}

173
/**
174
 * Implements hook_theme().
175 176 177 178
 */
function comment_theme() {
  return array(
    'comment_block' => array(
179
      'variables' => array('number' => NULL),
180 181
    ),
    'comment_preview' => array(
182
      'variables' => array('comment' => NULL),
183 184
    ),
    'comment' => array(
185
      'template' => 'comment',
186
      'render element' => 'elements',
187 188
    ),
    'comment_post_forbidden' => array(
189
      'variables' => array('node' => NULL),
190 191
    ),
    'comment_wrapper' => array(
192
      'template' => 'comment-wrapper',
193
      'render element' => 'content',
194 195 196 197
    ),
  );
}

Dries's avatar
 
Dries committed
198
/**
199
 * Implements hook_menu().
Dries's avatar
 
Dries committed
200
 */
201
function comment_menu() {
202
  $items['admin/content/comment'] = array(
203
    'title' => 'Comments',
204
    'description' => 'List and edit site comments and the comment approval queue.',
205 206
    'page callback' => 'comment_admin',
    'access arguments' => array('administer comments'),
207
    'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM,
208
    'file' => 'comment.admin.inc',
209
  );
210
  // Tabs begin here.
211
  $items['admin/content/comment/new'] = array(
212
    'title' => 'Published comments',
213 214
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
215
  $items['admin/content/comment/approval'] = array(
216 217
    'title' => 'Unapproved comments',
    'title callback' => 'comment_count_unpublished',
218
    'page arguments' => array('approval'),
219
    'access arguments' => array('administer comments'),
220 221
    'type' => MENU_LOCAL_TASK,
  );
222
  $items['comment/%comment'] = array(
223 224 225
    'title' => 'Comment permalink',
    'page callback' => 'comment_permalink',
    'page arguments' => array(1),
226 227
    'access callback' => 'entity_page_access',
    'access arguments' => array(1, 'view'),
228
  );
229
  $items['comment/%comment/view'] = array(
230 231 232
    'title' => 'View comment',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
233 234
  // 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).
235 236 237
  $items['comment/%comment/edit'] = array(
    'title' => 'Edit',
    'type' => MENU_LOCAL_TASK,
238
    'route_name' => 'comment_edit_page',
239
  );
240
  $items['comment/%comment/approve'] = array(
241 242 243
    'title' => 'Approve',
    'page callback' => 'comment_approve',
    'page arguments' => array(1),
244 245
    'access callback' => 'entity_page_access',
    'access arguments' => array(1, 'approve'),
246
    'file' => 'comment.pages.inc',
247
    'weight' => 10,
248
  );
249
  $items['comment/%comment/delete'] = array(
250
    'title' => 'Delete',
251 252
    'page callback' => 'comment_confirm_delete_page',
    'page arguments' => array(1),
253 254
    'access callback' => 'entity_page_access',
    'access arguments' => array(1, 'delete'),
255 256
    'type' => MENU_LOCAL_TASK,
    'file' => 'comment.admin.inc',
257
    'weight' => 20,
258
  );
259
  $items['comment/reply/%node'] = array(
260
    'title' => 'Add new comment',
261
    'page callback' => 'comment_reply',
262
    'page arguments' => array(2),
263 264
    'access callback' => 'node_access',
    'access arguments' => array('view', 2),
265
    'file' => 'comment.pages.inc',
266
  );
Dries's avatar
 
Dries committed
267 268 269 270

  return $items;
}

271 272 273 274 275
/**
 * Implements hook_menu_alter().
 */
function comment_menu_alter(&$items) {
  // Add comments to the description for admin/content.
276
  $items['admin/content']['description'] = 'Administer content and comments.';
277 278

  // Adjust the Field UI tabs on admin/structure/types/manage/[node-type].
279
  // See comment_entity_bundle_info().
280 281 282 283
  $items['admin/structure/types/manage/%/comment/fields']['title'] = 'Comment fields';
  $items['admin/structure/types/manage/%/comment/fields']['weight'] = 3;
  $items['admin/structure/types/manage/%/comment/display']['title'] = 'Comment display';
  $items['admin/structure/types/manage/%/comment/display']['weight'] = 4;
284 285
}

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

296
/**
297
 * Implements hook_node_type_insert().
298
 *
299 300
 * 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,
301 302 303
 * hook_modules_enabled() serves to create the body fields.
 *
 * @see comment_modules_enabled()
304
 */
305
function comment_node_type_insert($info) {
306
  _comment_body_field_create($info);
307
}
308

309
/**
310
 * Implements hook_node_type_update().
311 312 313
 */
function comment_node_type_update($info) {
  if (!empty($info->old_type) && $info->type != $info->old_type) {
314
    entity_invoke_bundle_hook('rename', 'comment', 'comment_node_' . $info->old_type, 'comment_node_' . $info->type);
315 316
  }
}
317

318
/**
319
 * Implements hook_node_type_delete().
320 321
 */
function comment_node_type_delete($info) {
322
  entity_invoke_bundle_hook('delete', 'comment', 'comment_node_' . $info->type);
323 324 325 326 327 328 329 330 331 332 333
  $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);
334 335 336
  }
}

337
 /**
338
 * Creates a comment_body field instance for a given node type.
339 340 341 342 343
 *
 * @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.
344
 */
345 346 347 348 349 350 351 352 353 354 355 356
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))) {
357
    entity_invoke_bundle_hook('create', 'comment', 'comment_node_' . $info->type);
358 359 360 361 362 363 364 365 366 367
    // 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,
    );
    field_create_instance($instance);
368 369 370 371 372 373 374 375 376

    // 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.
377 378 379 380 381 382 383
    entity_get_display('comment', 'comment_node_' . $info->type, 'default')
      ->setComponent('comment_body', array(
        'label' => 'hidden',
        'type' => 'text_default',
        'weight' => 0,
      ))
      ->save();
384
  }
385 386
}

Dries's avatar
 
Dries committed
387
/**
388
 * Implements hook_permission().
Dries's avatar
 
Dries committed
389
 */
390
function comment_permission() {
391
  return array(
392
    'administer comments' => array(
393
      'title' => t('Administer comments and comment settings'),
394 395
    ),
    'access comments' => array(
396
      'title' => t('View comments'),
397 398
    ),
    'post comments' => array(
399
      'title' => t('Post comments'),
400
    ),
401 402
    'skip comment approval' => array(
      'title' => t('Skip comment approval'),
403
    ),
404 405 406
    'edit own comments' => array(
      'title' => t('Edit own comments'),
    ),
407
  );
Dries's avatar
 
Dries committed
408 409
}

410 411 412 413 414 415 416 417 418 419 420
/**
 * 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.
 *
421 422
 * @param \Drupal\comment\Plugin\Core\Entity\Comment $comment
 *   A comment entity.
423
 *
424 425 426
 * @return
 *   The comment listing set to the page on which the comment appears.
 */
427 428
function comment_permalink(Comment $comment) {
  if ($node = $comment->nid->entity) {
429
    // Find the current display page for this comment.
430
    $page = comment_get_display_page($comment->id(), $node->type);
431

432
    // @todo: Cleaner sub request handling.
433 434
    $request = drupal_container()->get('request');
    $subrequest = Request::create('/node/' . $node->nid, 'GET', $request->query->all(), $request->cookies->all(), array(), $request->server->all());
435 436
    $subrequest->query->set('page', $page);
    // @todo: Convert the pager to use the request object.
437
    $_GET['page'] = $page;
438
    return drupal_container()->get('http_kernel')->handle($subrequest, HttpKernelInterface::SUB_REQUEST);
439
  }
440
  throw new NotFoundHttpException();
441 442
}

443
/**
444
 * Finds the most recent comments that are available to the current user.
445 446
 *
 * @param integer $number
447
 *   (optional) The maximum number of comments to find. Defaults to 10.
448
 *
449
 * @return
450 451
 *   An array of comment objects or an empty array if there are no recent
 *   comments visible to the current user.
452 453
 */
function comment_get_recent($number = 10) {
454
  $query = db_select('comment', 'c');
455
  $query->innerJoin('node_field_data', 'n', 'n.nid = c.nid');
456
  $query->addTag('node_access');
457
  $query->addMetaData('base_table', 'comment');
458 459 460 461
  $comments = $query
    ->fields('c')
    ->condition('c.status', COMMENT_PUBLISHED)
    ->condition('n.status', NODE_PUBLISHED)
462 463 464
    // @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)
465
    ->orderBy('c.created', 'DESC')
466 467 468
    // Additionally order by cid to ensure that comments with the same timestamp
    // are returned in the exact order posted.
    ->orderBy('c.cid', 'DESC')
469 470 471 472 473
    ->range(0, $number)
    ->execute()
    ->fetchAll();

  return $comments ? $comments : array();
474 475
}

476
/**
477
 * Calculates the page number for the first new comment.
478 479 480 481 482
 *
 * @param $num_comments
 *   Number of comments.
 * @param $new_replies
 *   Number of new replies.
483
 * @param \Drupal\Core\Entity\EntityInterface $node
484
 *   The first new comment node.
485
 *
486 487
 * @return
 *   "page=X" if the page number is greater than zero; empty string otherwise.
488
 */
489
function comment_new_page_count($num_comments, $new_replies, EntityInterface $node) {
490 491
  $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
  $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
492
  $pagenum = NULL;
493
  $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
494 495
  if ($num_comments <= $comments_per_page) {
    // Only one page of comments.
496
    $pageno = 0;
497
  }
498 499 500
  elseif ($flat) {
    // Flat comments.
    $count = $num_comments - $new_replies;
501
    $pageno = $count / $comments_per_page;
502
  }
503
  else {
504 505 506 507 508 509 510 511
    // 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)
512 513
      ->orderBy('created', 'DESC')
      ->orderBy('cid', 'DESC')
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
      ->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,
530
      ':nid' => $node->nid,
531
      ':thread' => $first_thread,
532
    ))->fetchField();
533

534
    $pageno = $count / $comments_per_page;
535
  }
536

537
  if ($pageno >= 1) {
538
    $pagenum = array('page' => intval($pageno));
539
  }
540

541 542 543
  return $pagenum;
}

544
/**
545
 * Returns HTML for a list of recent comments.
546 547 548
 *
 * @ingroup themeable
 */
549
function theme_comment_block($variables) {
550
  $items = array();
551
  $number = $variables['number'];
552
  foreach (comment_get_recent($number) as $comment) {
553
    $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>';
554
  }
555

556
  if ($items) {
557
    return theme('item_list', array('items' => $items));
558
  }
559 560 561
  else {
    return t('No comments available.');
  }
562 563
}

Dries's avatar
 
Dries committed
564
/**
565
 * Implements hook_node_view().
Dries's avatar
 
Dries committed
566
 */
567
function comment_node_view(EntityInterface $node, EntityDisplay $display, $view_mode) {
Dries's avatar
 
Dries committed
568 569
  $links = array();

570
  if ($node->comment != COMMENT_NODE_HIDDEN) {
571
    if ($view_mode == 'rss') {
572 573 574 575 576
      // 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))
      );
577
    }
578
    elseif ($view_mode == 'teaser') {
579 580 581
      // 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
582
      if (user_access('access comments')) {
583
        if (!empty($node->comment_count)) {
584
          $links['comment-comments'] = array(
585
            'title' => format_plural($node->comment_count, '1 comment', '@count comments'),
586 587
            'href' => "node/$node->nid",
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
588 589
            'fragment' => 'comments',
            'html' => TRUE,
590
          );
591 592
          // Show a link to the first new comment.
          if ($new = comment_num_new($node->nid)) {
593
            $links['comment-new-comments'] = array(
594
              'title' => format_plural($new, '1 new comment', '@count new comments'),
595
              'href' => "node/$node->nid",
596
              'query' => comment_new_page_count($node->comment_count, $new, $node),
597
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
598 599
              'fragment' => 'new',
              'html' => TRUE,
600
            );
Dries's avatar
 
Dries committed
601 602
          }
        }
603 604
      }
      if ($node->comment == COMMENT_NODE_OPEN) {
605
        $comment_form_location = variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW);
606 607 608
        if (user_access('post comments')) {
          $links['comment-add'] = array(
            'title' => t('Add new comment'),
609
            'href' => "node/$node->nid",
610 611 612
            'attributes' => array('title' => t('Add a new comment to this page.')),
            'fragment' => 'comment-form',
          );
613 614 615
          if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
            $links['comment-add']['href'] = "comment/reply/$node->nid";
          }
616
        }
Dries's avatar
 
Dries committed
617
        else {
618
          $links['comment-forbidden'] = array(
619 620 621
            'title' => theme('comment_post_forbidden', array('node' => $node)),
            'html' => TRUE,
          );
Dries's avatar
 
Dries committed
622 623 624
        }
      }
    }
625
    elseif ($view_mode != 'search_index' && $view_mode != 'search_result') {
626 627 628
      // 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
629
      // indexing or constructing a search result excerpt.
630
      if ($node->comment == COMMENT_NODE_OPEN) {
631
        $comment_form_location = variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW);
Dries's avatar
 
Dries committed
632
        if (user_access('post comments')) {
633 634 635 636 637 638 639 640 641 642 643 644
          // 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";
            }
645
          }
Dries's avatar
 
Dries committed
646 647
        }
        else {
648
          $links['comment-forbidden'] = array(
649 650 651
            'title' => theme('comment_post_forbidden', array('node' => $node)),
            'html' => TRUE,
          );
Dries's avatar
 
Dries committed
652 653 654
        }
      }
    }
Dries's avatar
Dries committed
655

656 657 658 659 660
    $node->content['links']['comment'] = array(
      '#theme' => 'links__node__comment',
      '#links' => $links,
      '#attributes' => array('class' => array('links', 'inline')),
    );
Dries's avatar
Dries committed
661

662 663 664
    // 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
665
    // displayed in 'full' view mode within another node.
666
    if ($node->comment && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) {
667
      $node->content['comments'] = comment_node_page_additions($node);
668
    }
669
  }
Dries's avatar
 
Dries committed
670 671
}

672
/**
673
 * Builds the comment-related elements for node detail pages.
674
 *
675
 * @param \Drupal\Core\Entity\EntityInterface $node
676
 *   The node entity for which to build the comment-related elements.
677 678 679 680
 *
 * @return
 *   A renderable array representing the comment-related page elements for the
 *   node.
681
 */
682
function comment_node_page_additions(EntityInterface $node) {
683 684 685 686 687
  $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.
688
  if (($node->comment_count && user_access('access comments')) || user_access('administer comments')) {
689 690 691
    $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)) {
692 693
      $comments = comment_load_multiple($cids);
      comment_prepare_thread($comments);
694
      $build = comment_view_multiple($comments);
695 696 697 698 699 700 701
      $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)) {
702
    $additions['comment_form'] = comment_add($node);
703 704 705 706
  }

  if ($additions) {
    $additions += array(
707
      '#theme' => 'comment_wrapper__node_' . $node->type,
708 709 710 711 712 713 714 715 716
      '#node' => $node,
      'comments' => array(),
      'comment_form' => array(),
    );
  }

  return $additions;
}

717 718 719
/**
 * Returns a rendered form to comment the given node.
 *
720
 * @param \Drupal\Core\Entity\EntityInterface $node
721
 *   The node entity to be commented.
722 723 724
 * @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.
725 726 727 728
 *
 * @return array
 *   The renderable array for the comment addition form.
 */
729
function comment_add(EntityInterface $node, $pid = NULL) {
730 731 732 733 734
  $values = array('nid' => $node->nid, 'pid' => $pid, 'node_type' => 'comment_node_' . $node->type);
  $comment = entity_create('comment', $values);
  return entity_get_form($comment);
}

735
/**
736
 * Retrieves comments for a thread.
737
 *
738
 * @param \Drupal\Core\Entity\EntityInterface $node
739
 *   The node whose comment(s) needs rendering.
740 741 742 743
 * @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.
744
 *
745 746 747
 * @return
 *   An array of the IDs of the comment to be displayed.
 *
748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801
 * 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.
 */
802
function comment_get_thread(EntityInterface $node, $mode, $comments_per_page) {
803 804
  $query = db_select('comment', 'c')
    ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
805 806 807 808
  $query->addField('c', 'cid');
  $query
    ->condition('c.nid', $node->nid)
    ->addTag('node_access')
809
    ->addTag('comment_filter')
810
    ->addMetaData('base_table', 'comment')
811
    ->addMetaData('node', $node)
812 813 814 815 816 817
    ->limit($comments_per_page);

  $count_query = db_select('comment', 'c');
  $count_query->addExpression('COUNT(*)');
  $count_query
    ->condition('c.nid', $node->nid)
818 819
    ->addTag('node_access')
    ->addTag('comment_filter')
820
    ->addMetaData('base_table', 'comment')
821
    ->addMetaData('node', $node);
822 823 824 825 826 827 828 829 830 831 832 833

  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.
834 835
    $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
    $query->orderBy('torder', 'ASC');
836 837 838 839 840 841 842 843 844
  }

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

  return $cids;
}

/**
845 846 847 848 849
 * 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.
850 851
 *
 * @param array $comments
852
 *   An array of comment objects, keyed by comment ID.
853 854 855 856 857 858 859 860 861
 */
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) {
862
    if ($first_new && $comment->new->value != MARK_READ) {
863 864 865 866 867 868 869 870
      // 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).
871
    $comment->depth = count(explode('.', $comment->thread->value)) - 1;
872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889
    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;
}

/**
890
 * Generates an array for rendering a comment.
891
 *
892
 * @param Drupal\comment\Comment $comment
893
 *   The comment object.
894 895
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...