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

Dries Buytaert's avatar
Dries Buytaert committed
3
4
/**
 * @file
Dries Buytaert's avatar
   
Dries Buytaert committed
5
 * Enables users to comment on published content.
Dries Buytaert's avatar
Dries Buytaert 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 Buytaert's avatar
Dries Buytaert committed
10
11
 */

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

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

/**
26
 * Comment is published.
27
 */
28
const COMMENT_PUBLISHED = 1;
29

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

/**
 * Comments are displayed as a threaded list - expanded.
 */
38
const COMMENT_MODE_THREADED = 1;
39
40

/**
41
 * Anonymous posters cannot enter their contact information.
42
 */
43
const COMMENT_ANONYMOUS_MAYNOT_CONTACT = 0;
44
45
46
47

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

/**
51
 * Anonymous posters are required to leave their contact information.
52
 */
53
const COMMENT_ANONYMOUS_MUST_CONTACT = 2;
54
55

/**
56
 * Comment form should be displayed on a separate page.
57
 */
58
const COMMENT_FORM_SEPARATE_PAGE = 0;
59
60
61
62

/**
 * Comment form should be shown below post or list of comments.
 */
63
const COMMENT_FORM_BELOW = 1;
64
65

/**
66
 * Comments for this node are hidden.
67
 */
68
const COMMENT_NODE_HIDDEN = 0;
69
70

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

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

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

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

101
/**
102
 * Implements hook_entity_view_mode_info().
103
 */
104
105
106
107
108
109
110
111
112
113
114
115
function comment_entity_view_mode_info() {
  $view_modes['comment']['full'] = array(
    'label' => t('Full comment'),
  );
  return $view_modes;
}

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

138
/**
139
 * Loads the comment bundle name corresponding a given content type.
140
 *
141
142
143
 * This function is used as a menu loader callback in comment_menu().
 *
 * @param $name
144
 *   The machine name of the node type whose comment fields are to be edited.
145
146
147
148
149
 *
 * @return
 *   The comment bundle name corresponding to the node type.
 *
 * @see comment_menu_alter()
150
151
 */
function comment_node_type_load($name) {
152
  if ($type = node_type_load($name)) {
153
154
155
156
    return 'comment_node_' . $type->type;
  }
}

157
/**
158
 * Entity URI callback.
159
 */
160
function comment_uri(Comment $comment) {
161
  return array(
162
163
    'path' => 'comment/' . $comment->id(),
    'options' => array('fragment' => 'comment-' . $comment->id()),
164
  );
165
166
}

167
168
169
170
171
172
173
174
175
/**
 * 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(
176
177
178
179
180
181
        'form' => array(
          'author' => array(
            'label' => t('Author'),
            'description' => t('Author textfield'),
            'weight' => -2,
          ),
182
          'subject' => array(
183
184
185
186
            'label' => t('Subject'),
            'description' => t('Subject textfield'),
            'weight' => -1,
          ),
187
188
189
190
191
192
193
194
        ),
      );
    }
  }

  return $return;
}

195
/**
196
 * Implements hook_theme().
197
198
199
200
 */
function comment_theme() {
  return array(
    'comment_block' => array(
201
      'variables' => array('number' => NULL),
202
203
    ),
    'comment_preview' => array(
204
      'variables' => array('comment' => NULL),
205
206
    ),
    'comment' => array(
207
      'template' => 'comment',
208
      'render element' => 'elements',
209
210
    ),
    'comment_post_forbidden' => array(
211
      'variables' => array('node' => NULL),
212
213
    ),
    'comment_wrapper' => array(
214
      'template' => 'comment-wrapper',
215
      'render element' => 'content',
216
217
218
219
    ),
  );
}

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

  return $items;
}

296
297
298
299
300
/**
 * Implements hook_menu_alter().
 */
function comment_menu_alter(&$items) {
  // Add comments to the description for admin/content.
301
  $items['admin/content']['description'] = 'Administer content and comments.';
302
303

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

311
312
313
314
315
316
317
318
319
320
/**
 * 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));
}

321
/**
322
 * Implements hook_node_type_insert().
323
 *
324
325
 * 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,
326
327
328
 * hook_modules_enabled() serves to create the body fields.
 *
 * @see comment_modules_enabled()
329
 */
330
function comment_node_type_insert($info) {
331
  _comment_body_field_create($info);
332
}
333

334
/**
335
 * Implements hook_node_type_update().
336
337
338
 */
function comment_node_type_update($info) {
  if (!empty($info->old_type) && $info->type != $info->old_type) {
339
    field_attach_rename_bundle('comment', 'comment_node_' . $info->old_type, 'comment_node_' . $info->type);
340
341
  }
}
342

343
/**
344
 * Implements hook_node_type_delete().
345
346
 */
function comment_node_type_delete($info) {
347
  field_attach_delete_bundle('comment', 'comment_node_' . $info->type);
348
349
350
351
352
353
354
355
356
357
358
  $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);
359
360
361
  }
}

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

Dries Buytaert's avatar
   
Dries Buytaert committed
403
/**
404
 * Implements hook_permission().
Dries Buytaert's avatar
   
Dries Buytaert committed
405
 */
406
function comment_permission() {
407
  return array(
408
    'administer comments' => array(
409
      'title' => t('Administer comments and comment settings'),
410
411
    ),
    'access comments' => array(
412
      'title' => t('View comments'),
413
414
    ),
    'post comments' => array(
415
      'title' => t('Post comments'),
416
    ),
417
418
    'skip comment approval' => array(
      'title' => t('Skip comment approval'),
419
    ),
420
421
422
    'edit own comments' => array(
      'title' => t('Edit own comments'),
    ),
423
  );
Dries Buytaert's avatar
   
Dries Buytaert committed
424
425
}

426
427
428
429
430
431
432
433
434
435
436
/**
 * 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.
 *
437
438
 * @param $cid
 *   A comment identifier.
439
 *
440
441
442
 * @return
 *   The comment listing set to the page on which the comment appears.
 */
443
function comment_permalink($cid) {
444
  if (($comment = comment_load($cid)) && ($node = $comment->nid->entity)) {
445
446

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

449
    // @todo: Cleaner sub request handling.
450
451
    $request = drupal_container()->get('request');
    $subrequest = Request::create('/node/' . $node->nid, 'GET', $request->query->all(), $request->cookies->all(), array(), $request->server->all());
452
453
    $subrequest->query->set('page', $page);
    // @todo: Convert the pager to use the request object.
454
    $_GET['page'] = $page;
455
    return drupal_container()->get('http_kernel')->handle($subrequest, HttpKernelInterface::SUB_REQUEST);
456
  }
457
  throw new NotFoundHttpException();
458
459
}

460
/**
461
 * Finds the most recent comments that are available to the current user.
462
463
 *
 * @param integer $number
464
 *   (optional) The maximum number of comments to find. Defaults to 10.
465
 *
466
 * @return
467
468
 *   An array of comment objects or an empty array if there are no recent
 *   comments visible to the current user.
469
470
 */
function comment_get_recent($number = 10) {
471
472
473
  $query = db_select('comment', 'c');
  $query->innerJoin('node', 'n', 'n.nid = c.nid');
  $query->addTag('node_access');
474
  $query->addMetaData('base_table', 'comment');
475
476
477
478
  $comments = $query
    ->fields('c')
    ->condition('c.status', COMMENT_PUBLISHED)
    ->condition('n.status', NODE_PUBLISHED)
479
    ->orderBy('c.created', 'DESC')
480
481
482
    // Additionally order by cid to ensure that comments with the same timestamp
    // are returned in the exact order posted.
    ->orderBy('c.cid', 'DESC')
483
484
485
486
487
    ->range(0, $number)
    ->execute()
    ->fetchAll();

  return $comments ? $comments : array();
488
489
}

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

548
    $pageno = $count / $comments_per_page;
549
  }
550

551
  if ($pageno >= 1) {
552
    $pagenum = array('page' => intval($pageno));
553
  }
554

555
556
557
  return $pagenum;
}

558
/**
559
 * Returns HTML for a list of recent comments.
560
561
562
 *
 * @ingroup themeable
 */
563
function theme_comment_block($variables) {
564
  $items = array();
565
  $number = $variables['number'];
566
  foreach (comment_get_recent($number) as $comment) {
567
    $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>';
568
  }
569

570
  if ($items) {
571
    return theme('item_list', array('items' => $items));
572
  }
573
574
575
  else {
    return t('No comments available.');
  }
576
577
}

Dries Buytaert's avatar
   
Dries Buytaert committed
578
/**
579
 * Implements hook_node_view().
Dries Buytaert's avatar
   
Dries Buytaert committed
580
 */
581
function comment_node_view(Node $node, EntityDisplay $display, $view_mode) {
Dries Buytaert's avatar
   
Dries Buytaert committed
582
583
  $links = array();

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

670
671
672
673
674
    $node->content['links']['comment'] = array(
      '#theme' => 'links__node__comment',
      '#links' => $links,
      '#attributes' => array('class' => array('links', 'inline')),
    );
Dries Buytaert's avatar
Dries Buytaert committed
675

676
677
678
    // 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
679
    // displayed in 'full' view mode within another node.
680
    if ($node->comment && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) {
681
      $node->content['comments'] = comment_node_page_additions($node);
682
    }
683
  }
Dries Buytaert's avatar
   
Dries Buytaert committed
684
685
}

686
/**
687
 * Builds the comment-related elements for node detail pages.
688
 *
689
690
 * @param Drupal\node\Node $node
 *   The node entity for which to build the comment-related elements.
691
692
693
694
 *
 * @return
 *   A renderable array representing the comment-related page elements for the
 *   node.
695
 */
696
function comment_node_page_additions(Node $node) {
697
698
699
700
701
  $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.
702
  if (($node->comment_count && user_access('access comments')) || user_access('administer comments')) {
703
704
705
    $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)) {
706
707
      $comments = comment_load_multiple($cids);
      comment_prepare_thread($comments);
708
      $build = comment_view_multiple($comments);
709
710
711
712
713
714
715
      $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)) {
716
    $additions['comment_form'] = comment_add($node);
717
718
719
720
  }

  if ($additions) {
    $additions += array(
721
      '#theme' => 'comment_wrapper__node_' . $node->type,
722
723
724
725
726
727
728
729
730
      '#node' => $node,
      'comments' => array(),
      'comment_form' => array(),
    );
  }

  return $additions;
}

731
732
733
734
735
/**
 * Returns a rendered form to comment the given node.
 *
 * @param Drupal\node\Node $node
 *   The node entity to be commented.
736
737
738
 * @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.
739
740
741
742
743
744
745
746
747
748
 *
 * @return array
 *   The renderable array for the comment addition form.
 */
function comment_add(Node $node, $pid = NULL) {
  $values = array('nid' => $node->nid, 'pid' => $pid, 'node_type' => 'comment_node_' . $node->type);
  $comment = entity_create('comment', $values);
  return entity_get_form($comment);
}

749
/**
750
 * Retrieves comments for a thread.
751
 *
752
 * @param Drupal\node\Node $node
753
 *   The node whose comment(s) needs rendering.
754
755
756
757
 * @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.
758
 *
759
760
761
 * @return
 *   An array of the IDs of the comment to be displayed.
 *
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
802
803
804
805
806
807
808
809
810
811
812
813
814
815
 * 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.
 */
816
function comment_get_thread(Node $node, $mode, $comments_per_page) {
817
818
  $query = db_select('comment', 'c')
    ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
819
820
821
822
  $query->addField('c', 'cid');
  $query
    ->condition('c.nid', $node->nid)
    ->addTag('node_access')
823
    ->addTag('comment_filter')
824
    ->addMetaData('base_table', 'comment')
825
    ->addMetaData('node', $node)
826
827
828
829
830
831
    ->limit($comments_per_page);

  $count_query = db_select('comment', 'c');
  $count_query->addExpression('COUNT(*)');
  $count_query
    ->condition('c.nid', $node->nid)
832
833
    ->addTag('node_access')
    ->addTag('comment_filter')
834
    ->addMetaData('base_table', 'comment')
835
    ->addMetaData('node', $node);
836
837
838
839
840
841
842
843
844
845
846
847

  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.
848
849
    $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
    $query->orderBy('torder', 'ASC');
850
851
852
853
854
855
856
857
858
  }

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

  return $cids;
}

/**
859
860
861
862
863
 * 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.
864
865
 *
 * @param array $comments
866
 *   An array of comment objects, keyed by comment ID.
867
868
869
870
871
872
873
874
875
 */
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) {
876
    if ($first_new && $comment->new->value != MARK_READ) {
877
878
879
880
881
882
883
884
      // 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).
885
    $comment->depth = count(explode('.', $comment->thread->value)) - 1;
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
    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;
}

/**
904
 * Generates an array for rendering a comment.
905
 *
906
 * @param Drupal\comment\Comment $comment
907
 *   The comment object.
908
909
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
910
911
912
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
913
914
915
916
 *
 * @return
 *   An array as expected by drupal_render().
 */
917
918
function comment_view(Comment $comment, $view_mode = 'full', $langcode = NULL) {
  return entity_view($comment, $view_mode, $langcode);
919
920
}

921
/**
922
 * Adds reply, edit, delete, etc. links, depending on user permissions.
923
 *
924
 * @param Drupal\comment\Comment $comment
925
 *   The comment object.
926
 * @param Drupal\node\Node $node
927
 *   The node the comment is attached to.
928
 *
929
930
931
 * @return
 *   A structured array of links.
 */
932
function comment_links(Comment $comment, Node $node) {
933
934
935
  $links = array();
  if ($node->comment == COMMENT_NODE_OPEN) {
    if (user_access('administer comments') && user_access('post comments')) {
936
      $links['comment-delete'] = array(
937
        'title' => t('delete'),
938
        'href' => "comment/{$comment->id()}/delete",
939
940
        'html' => TRUE,
      );
941