comment.module 82.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
/**
13
 * Comment is awaiting approval.
14
 */
15
const COMMENT_NOT_PUBLISHED = 0;
16
17

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

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

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

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

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

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

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

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

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

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

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

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

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

  foreach (node_type_get_names() as $type => $name) {
    $return['comment']['bundles']['comment_node_' . $type] = array(
121
      'label' => t('@node_type comment', array('@node_type' => $name)),
122
123
124
      // 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,
125
126
127
128
129
130
      '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.
131
        // See comment_node_type_load() and comment_menu_alter().
132
133
134
135
136
        'path' => 'admin/structure/types/manage/%comment_node_type/comment',
        'bundle argument' => 4,
        'real path' => 'admin/structure/types/manage/' . str_replace('_', '-', $type) . '/comment',
        'access arguments' => array('administer content types'),
      ),
137
138
139
140
141
142
    );
  }

  return $return;
}

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

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

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

  return $return;
}

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

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

  return $items;
}

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

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

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

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

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

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

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

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

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

440
441
  return $blocks;
}
442

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

454
455
  return $form;
}
456

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

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

474
    return $block;
Dries's avatar
   
Dries committed
475
476
477
  }
}

478
479
480
481
482
483
484
485
486
487
488
/**
 * 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.
 *
489
490
 * @param $cid
 *   A comment identifier.
491
 *
492
493
494
 * @return
 *   The comment listing set to the page on which the comment appears.
 */
495
496
function comment_permalink($cid) {
  if (($comment = comment_load($cid)) && ($node = node_load($comment->nid))) {
497
498
499
500
501
502
503
504
505
506

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

    // Set $_GET['q'] and $_GET['page'] ourselves so that the node callback
    // behaves as it would when visiting the page directly.
    $_GET['q'] = 'node/' . $node->nid;
    $_GET['page'] = $page;

    // Return the node view, this will show the correct comment in context.
507
    return menu_execute_active_handler('node/' . $node->nid, FALSE);
508
509
510
511
  }
  drupal_not_found();
}

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

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

541
/**
542
 * Calculates the page number for the first new comment.
543
544
545
546
547
548
549
 *
 * @param $num_comments
 *   Number of comments.
 * @param $new_replies
 *   Number of new replies.
 * @param $node
 *   The first new comment node.
550
 *
551
552
 * @return
 *   "page=X" if the page number is greater than zero; empty string otherwise.
553
 */
554
function comment_new_page_count($num_comments, $new_replies, $node) {
555
556
  $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
  $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
557
  $pagenum = NULL;
558
  $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
559
560
  if ($num_comments <= $comments_per_page) {
    // Only one page of comments.
561
    $pageno = 0;
562
  }
563
564
565
  elseif ($flat) {
    // Flat comments.
    $count = $num_comments - $new_replies;
566
    $pageno = $count / $comments_per_page;
567
  }
568
  else {
569
570
571
572
573
574
575
576
    // Threaded comments: we build a query with a subquery to find the first
    // thread with a new comment.

    // 1. Find all the threads with a new comment.
    $unread_threads_query = db_select('comment')
      ->fields('comment', array('thread'))
      ->condition('nid', $node->nid)
      ->condition('status', COMMENT_PUBLISHED)
577
578
      ->orderBy('created', 'DESC')
      ->orderBy('cid', 'DESC')
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
      ->range(0, $new_replies);

    // 2. Find the first thread.
    $first_thread = db_select($unread_threads_query, 'thread')
      ->fields('thread', array('thread'))
      ->orderBy('SUBSTRING(thread, 1, (LENGTH(thread) - 1))')
      ->range(0, 1)
      ->execute()
      ->fetchField();

    // Remove the final '/'.
    $first_thread = substr($first_thread, 0, -1);

    // Find the number of the first comment of the first unread thread.
    $count = db_query('SELECT COUNT(*) FROM {comment} WHERE nid = :nid AND status = :status AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread', array(
      ':status' => COMMENT_PUBLISHED,
595
      ':nid' => $node->nid,
596
      ':thread' => $first_thread,
597
    ))->fetchField();
598

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

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

606
607
608
  return $pagenum;
}

609
/**
610
 * Returns HTML for a list of recent comments.
611
612
613
 *
 * @ingroup themeable
 */
614
615
function theme_comment_block() {
  $items = array();
616
617
  $number = variable_get('comment_block_count', 10);
  foreach (comment_get_recent($number) as $comment) {
618
    $items[] = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)) . '&nbsp;<span>' . t('@time ago', array('@time' => format_interval(REQUEST_TIME - $comment->changed))) . '</span>';
619
  }
620

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

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

635
  if ($node->comment != COMMENT_NODE_HIDDEN) {
636
    if ($view_mode == 'rss') {
637
638
639
640
641
      // Add a comments RSS element which is a URL to the comments of this node.
      $node->rss_elements[] = array(
        'key' => 'comments',
        'value' => url('node/' . $node->nid, array('fragment' => 'comments', 'absolute' => TRUE))
      );
642
    }
643
    elseif ($view_mode == 'teaser') {
644
645
646
      // Teaser view: display the number of comments that have been posted,
      // or a link to add new comments if the user has permission, the node
      // is open to new comments, and there currently are none.
Dries's avatar
   
Dries committed
647
      if (user_access('access comments')) {
648
        if (!empty($node->comment_count)) {
649
          $links['comment-comments'] = array(
650
            'title' => format_plural($node->comment_count, '1 comment', '@count comments'),
651
652
            'href' => "node/$node->nid",
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
653
654
            'fragment' => 'comments',
            'html' => TRUE,
655
          );
656
657
          // Show a link to the first new comment.
          if ($new = comment_num_new($node->nid)) {
658
            $links['comment-new-comments'] = array(
659
              'title' => format_plural($new, '1 new comment', '@count new comments'),
660
              'href' => "node/$node->nid",
661
              'query' => comment_new_page_count($node->comment_count, $new, $node),
662
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
663
664
              'fragment' => 'new',
              'html' => TRUE,
665
            );
Dries's avatar
   
Dries committed
666
667
          }
        }
668
669
      }
      if ($node->comment == COMMENT_NODE_OPEN) {
670
        $comment_form_location = variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW);
671
672
673
        if (user_access('post comments')) {
          $links['comment-add'] = array(
            'title' => t('Add new comment'),
674
            'href' => "node/$node->nid",
675
676
677
            'attributes' => array('title' => t('Add a new comment to this page.')),
            'fragment' => 'comment-form',
          );
678
679
680
          if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
            $links['comment-add']['href'] = "comment/reply/$node->nid";
          }
681
        }
Dries's avatar
   
Dries committed
682
        else {
683
          $links['comment-forbidden'] = array(
684
685
686
            'title' => theme('comment_post_forbidden', array('node' => $node)),
            'html' => TRUE,
          );
Dries's avatar
   
Dries committed
687
688
689
        }
      }
    }
690
    elseif ($view_mode != 'search_index' && $view_mode != 'search_result') {
691
692
693
      // Node in other view modes: add a "post comment" link if the user is
      // allowed to post comments and if this node is allowing new comments.
      // But we don't want this link if we're building the node for search
694
      // indexing or constructing a search result excerpt.
695
      if ($node->comment == COMMENT_NODE_OPEN) {
696
        $comment_form_location = variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW);
Dries's avatar
   
Dries committed
697
        if (user_access('post comments')) {
698
699
700
701
702
703
704
705
706
707
708
709
          // Show the "post comment" link if the form is on another page, or
          // if there are existing comments that the link will skip past.
          if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE || (!empty($node->comment_count) && user_access('access comments'))) {
            $links['comment-add'] = array(
              'title' => t('Add new comment'),
              'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
              'href' => "node/$node->nid",
              'fragment' => 'comment-form',
            );
            if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
              $links['comment-add']['href'] = "comment/reply/$node->nid";
            }
710
          }
Dries's avatar
   
Dries committed
711
712
        }
        else {
713
          $links['comment-forbidden'] = array(
714
715
716
            'title' => theme('comment_post_forbidden', array('node' => $node)),
            'html' => TRUE,
          );
Dries's avatar
   
Dries committed
717
718
719
        }
      }
    }
Dries's avatar
Dries committed
720

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

727
728
729
    // Only append comments when we are building a node on its own node detail
    // page. We compare $node and $page_node to ensure that comments are not
    // appended to other nodes shown on the page, for example a node_reference
730
    // displayed in 'full' view mode within another node.
731
    if ($node->comment && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) {
732
      $node->content['comments'] = comment_node_page_additions($node);
733
    }
734
  }
Dries's avatar
   
Dries committed
735
736
}

737
/**
738
 * Builds the comment-related elements for node detail pages.
739
740
 *
 * @param $node
741
742
743
744
745
 *   The node object for which to build the comment-related elements.
 *
 * @return
 *   A renderable array representing the comment-related page elements for the
 *   node.
746
 */
747
function comment_node_page_additions($node) {
748
749
750
751
752
  $additions = array();

  // Only attempt to render comments if the node has visible comments.
  // Unpublished comments are not included in $node->comment_count, so show
  // comments unconditionally if the user is an administrator.
753
  if (($node->comment_count && user_access('access comments')) || user_access('administer comments')) {
754
755
756
    $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
    $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
    if ($cids = comment_get_thread($node, $mode, $comments_per_page)) {
757
758
      $comments = comment_load_multiple($cids);
      comment_prepare_thread($comments);
759
      $build = comment_view_multiple($comments, $node);
760
761
762
763
764
765
766
      $build['pager']['#theme'] = 'pager';
      $additions['comments'] = $build;
    }
  }

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

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

  return $additions;
}

/**
784
 * Retrieves comments for a thread.
785
786
787
 *
 * @param $node
 *   The node whose comment(s) needs rendering.
788
789
790
791
 * @param $mode
 *   The comment display mode; COMMENT_MODE_FLAT or COMMENT_MODE_THREADED.
 * @param $comments_per_page
 *   The amount of comments to display per page.
792
 *
793
794
795
 * @return
 *   An array of the IDs of the comment to be displayed.
 *
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
 * To display threaded comments in the correct order we keep a 'thread' field
 * and order by that value. This field keeps this data in
 * a way which is easy to update and convenient to use.
 *
 * A "thread" value starts at "1". If we add a child (A) to this comment,
 * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
 * brother of (A) will get "1.2". Next brother of the parent of (A) will get
 * "2" and so on.
 *
 * First of all note that the thread field stores the depth of the comment:
 * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
 *
 * Now to get the ordering right, consider this example:
 *
 * 1
 * 1.1
 * 1.1.1
 * 1.2
 * 2
 *
 * If we "ORDER BY thread ASC" we get the above result, and this is the
 * natural order sorted by time. However, if we "ORDER BY thread DESC"
 * we get:
 *
 * 2
 * 1.2
 * 1.1.1
 * 1.1
 * 1
 *
 * Clearly, this is not a natural way to see a thread, and users will get
 * confused. The natural order to show a thread by time desc would be:
 *
 * 2
 * 1
 * 1.2
 * 1.1
 * 1.1.1
 *
 * which is what we already did before the standard pager patch. To achieve
 * this we simply add a "/" at the end of each "thread" value. This way, the
 * thread fields will look like this:
 *
 * 1/
 * 1.1/
 * 1.1.1/
 * 1.2/
 * 2/
 *
 * we add "/" since this char is, in ASCII, higher than every number, so if
 * now we "ORDER BY thread DESC" we get the correct order. However this would
 * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
 * to consider the trailing "/" so we use a substring only.
 */
850
function comment_get_thread($node, $mode, $comments_per_page) {
851
852
853
854
855
  $query = db_select('comment', 'c')->extend('PagerDefault');
  $query->addField('c', 'cid');
  $query
    ->condition('c.nid', $node->nid)
    ->addTag('node_access')
856
857
    ->addTag('comment_filter')
    ->addMetaData('node', $node)
858
859
860
861
862
863
    ->limit($comments_per_page);

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

  if (!user_access('administer comments')) {
    $query->condition('c.status', COMMENT_PUBLISHED);
    $count_query->condition('c.status', COMMENT_PUBLISHED);
  }
  if ($mode === COMMENT_MODE_FLAT) {
    $query->orderBy('c.cid', 'ASC');
  }
  else {
    // See comment above. Analysis reveals that this doesn't cost too
    // much. It scales much much better than having the whole comment
    // structure.
879
880
    $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
    $query->orderBy('torder', 'ASC');
881
882
883
884
885
886
887
888
889
  }

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

  return $cids;
}

/**
890
891
892
893
894
 * Calculates the indentation level of each comment in a comment thread.
 *
 * This function loops over an array representing a comment thread. For each
 * comment, the function calculates the indentation level and saves it in the
 * 'divs' property of the comment object.
895
896
 *
 * @param array $comments
897
 *   An array of comment objects, keyed by comment ID.
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
 */
function comment_prepare_thread(&$comments) {
  // A flag stating if we are still searching for first new comment on the thread.
  $first_new = TRUE;

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

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

    // The $divs element instructs #prefix whether to add an indent div or
    // close existing divs (a negative value).
    $comment->depth = count(explode('.', $comment->thread)) - 1;
    if ($comment->depth > $divs) {
      $comment->divs = 1;
      $divs++;
    }
    else {
      $comment->divs = $comment->depth - $divs;
      while ($comment->depth < $divs) {
        $divs--;
      }
    }
    $comments[$key] = $comment;
  }

  // The final comment must close up some hanging divs
  $comments[$key]->divs_final = $divs;
}

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

955
  // Populate $comment->content with a render() array.
956
  comment_build_content($comment, $node, $view_mode, $langcode);
957
958

  $build = $comment->content;
959
960
  // We don't need duplicate rendering info in comment->content.
  unset($comment->content);
961
962

  $build += array(
963
    '#theme' => 'comment__node_' . $node->type,
964
    '#comment' => $comment,
965
    '#node' => $node,
966
    '#view_mode' => $view_mode,
967
    '#language' => $langcode,
968
969
  );

970
971
972
  if (empty($comment->in_preview)) {
    $prefix = '';
    $is_threaded = isset($comment->divs) && variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED) == COMMENT_MODE_THREADED;
973

974
975
976
977
    // Add 'new' anchor if needed.
    if (!empty($comment->first_new)) {
      $prefix .= "<a id=\"new\"></a>\n";
    }
978

979
980
981
982
    // Add indentation div or close open divs as needed.
    if ($is_threaded) {
      $prefix .= $comment->divs <= 0 ? str_repeat('</div>', abs($comment->divs)) : "\n" . '<div class="indented">';
    }
983

984
985
986
    // Add anchor for each comment.
    $prefix .= "<a id=\"comment-$comment->cid\"></a>\n";
    $build['#prefix'] = $prefix;
987

988
989
990
991
    // Close all open divs.
    if ($is_threaded && !empty($comment->divs_final)) {
      $build['#suffix'] = str_repeat('</div>', $comment->divs_final);
    }
992
993
  }

994
  // Allow modules to modify the structured comment.
995
996
  $type = 'comment';
  drupal_alter(array('comment_view', 'entity_view'), $build, $type);
997