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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

81
use Drupal\comment\Entity\Comment;
82

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

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

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

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

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

  return $return;
}

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

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

  return $items;
}

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

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

260 261 262 263 264 265 266 267 268 269
/**
 * 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));
}

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

283
/**
284
 * Implements hook_node_type_update().
285
 */
286 287 288
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());
289 290
  }
}
291

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

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

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

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

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

  return $comments ? $comments : array();
415 416
}

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

475
    $pageno = $count / $comments_per_page;
476
  }
477

478
  if ($pageno >= 1) {
479
    $pagenum = array('page' => intval($pageno));
480
  }
481

482 483 484
  return $pagenum;
}

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

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

Dries's avatar
 
Dries committed
509
/**
510
 * Implements hook_node_view().
Dries's avatar
 
Dries committed
511
 */
512
function comment_node_view(EntityInterface $node, EntityDisplay $display, $view_mode) {
Dries's avatar
 
Dries committed
513 514
  $links = array();

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

609 610 611 612 613
    $node->content['links']['comment'] = array(
      '#theme' => 'links__node__comment',
      '#links' => $links,
      '#attributes' => array('class' => array('links', 'inline')),
    );
Dries's avatar
Dries committed
614

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

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

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

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

  return $additions;
}

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

688
/**
689
 * Retrieves comments for a thread.
690
 *
691
 * @param \Drupal\Core\Entity\EntityInterface $node
692
 *   The node whose comment(s) needs rendering.
693 694 695 696
 * @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.
697
 *
698 699 700
 * @return
 *   An array of the IDs of the comment to be displayed.
 *
701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754
 * 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.
 */
755
function comment_get_thread(EntityInterface $node, $mode, $comments_per_page) {
756 757
  $query = db_select('comment', 'c')
    ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
758 759
  $query->addField('c', 'cid');
  $query
760
    ->condition('c.nid', $node->id())
761
    ->addTag('node_access')
762
    ->addTag('comment_filter')
763
    ->addMetaData('base_table', 'comment')
764
    ->addMetaData('node', $node)
765 766