comment.module 89.5 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
 *
 * When enabled, the Drupal comment module creates a discussion
 * board for each Drupal node. Users can post comments to discuss
9
 * a forum topic, story, collaborative book page, etc.
Dries's avatar
Dries committed
10
11
 */

12
/**
13
 * Comment is awaiting approval.
14
 */
15
define('COMMENT_NOT_PUBLISHED', 0);
16
17

/**
18
 * Comment is published.
19
 */
20
define('COMMENT_PUBLISHED', 1);
Dries's avatar
Dries committed
21

22
23
24
/**
 * Comments are displayed in a flat list - expanded.
 */
25
define('COMMENT_MODE_FLAT', 0);
26
27
28
29

/**
 * Comments are displayed as a threaded list - expanded.
 */
30
define('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
 */
define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0);
36
37
38
39

/**
 * Anonymous posters may leave their contact information.
 */
Dries's avatar
Dries committed
40
define('COMMENT_ANONYMOUS_MAY_CONTACT', 1);
41
42

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

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

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

/**
58
 * Comments for this node are hidden.
Dries's avatar
Dries committed
59
 */
60
define('COMMENT_NODE_HIDDEN', 0);
61
62

/**
63
 * Comments for this node are closed.
64
 */
65
define('COMMENT_NODE_CLOSED', 1);
66
67

/**
68
 * Comments for this node are open.
69
 */
70
define('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
79
80
81
82
      $output = '<h3>' . t('About') . '</h3>';
      $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/handbook/modules/comment/')) . '</p>';
      $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
101
      'fieldable' => TRUE,
      'controller class' => 'CommentController',
102
      'entity keys' => array(
103
104
        'id' => 'cid',
        'bundle' => 'node_type',
105
        'label' => 'subject',
106
107
      ),
      'bundles' => array(),
108
109
110
      'view modes' => array(
        'full' => array(
          'label' => t('Full comment'),
111
          'custom settings' => FALSE,
112
113
        ),
      ),
114
115
116
117
118
119
      'static cache' => FALSE,
    ),
  );

  foreach (node_type_get_names() as $type => $name) {
    $return['comment']['bundles']['comment_node_' . $type] = array(
120
      'label' => t('@node_type comment', array('@node_type' => $name)),
121
122
123
      // 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,
124
125
126
127
128
129
      '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.
130
        // See comment_node_type_load() and comment_menu_alter().
131
132
133
134
135
        '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'),
      ),
136
137
138
139
140
141
    );
  }

  return $return;
}

142
143
144
145
146
147
148
149
150
151
152
/**
 * Menu loader callback for Field UI paths.
 *
 * Return a comment bundle name from a node type in the URL.
 */
function comment_node_type_load($name) {
  if ($type = node_type_get_type(strtr($name, array('-' => '_')))) {
    return 'comment_node_' . $type->type;
  }
}

153
/**
154
 * Entity uri callback.
155
 */
156
157
158
159
160
function comment_uri($comment) {
  return array(
    'path' => 'comment/' . $comment->cid,
    'options' => array('fragment' => 'comment-' . $comment->cid),
  );
161
162
}

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

  return $return;
}

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

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

  return $items;
}

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

  // 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;
}

307
308
309
310
311
312
313
314
315
316
/**
 * 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));
}

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

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

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

358
 /**
359
 * Creates a comment_body field instance for a given node type.
360
 */
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
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,
        ),
388
      ),
389
390
391
    );
    field_create_instance($instance);
  }
392
393
}

Dries's avatar
   
Dries committed
394
/**
395
 * Implements hook_permission().
Dries's avatar
   
Dries committed
396
 */
397
function comment_permission() {
398
  return array(
399
    'administer comments' => array(
400
      'title' => t('Administer comments and comment settings'),
401
402
    ),
    'access comments' => array(
403
      'title' => t('View comments'),
404
405
    ),
    'post comments' => array(
406
      'title' => t('Post comments'),
407
    ),
408
409
    'skip comment approval' => array(
      'title' => t('Skip comment approval'),
410
    ),
411
412
413
    'edit own comments' => array(
      'title' => t('Edit own comments'),
    ),
414
  );
Dries's avatar
   
Dries committed
415
416
417
}

/**
418
 * Implements hook_block_info().
Dries's avatar
   
Dries committed
419
 */
420
function comment_block_info() {
421
  $blocks['recent']['info'] = t('Recent comments');
422
  $blocks['recent']['properties']['administrative'] = TRUE;
423

424
425
  return $blocks;
}
426

427
/**
428
 * Implements hook_block_configure().
429
430
431
432
433
434
435
436
 */
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)),
  );
437

438
439
  return $form;
}
440

441
/**
442
 * Implements hook_block_save().
443
444
 */
function comment_block_save($delta = '', $edit = array()) {
445
  variable_set('comment_block_count', (int) $edit['comment_block_count']);
446
}
447

448
/**
449
 * Implements hook_block_view().
450
451
452
453
454
455
456
 *
 * 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');
457

458
    return $block;
Dries's avatar
   
Dries committed
459
460
461
  }
}

462
463
464
465
466
467
468
469
470
471
472
/**
 * 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.
 *
473
474
 * @param $cid
 *   A comment identifier.
475
476
477
 * @return
 *   The comment listing set to the page on which the comment appears.
 */
478
479
function comment_permalink($cid) {
  if (($comment = comment_load($cid)) && ($node = node_load($comment->nid))) {
480
481
482
483
484
485
486
487
488
489

    // 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.
490
    return menu_execute_active_handler('node/' . $node->nid, FALSE);
491
492
493
494
  }
  drupal_not_found();
}

495
/**
496
497
498
 * Find the most recent comments that are available to the current user.
 *
 * @param integer $number
499
 *   (optional) The maximum number of comments to find. Defaults to 10.
500
 *
501
 * @return
502
503
 *   An array of comment objects or an empty array if there are no recent
 *   comments visible to the current user.
504
505
 */
function comment_get_recent($number = 10) {
506
507
508
509
510
511
512
  $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)
513
    ->orderBy('c.created', 'DESC')
514
515
516
    // Additionally order by cid to ensure that comments with the same timestamp
    // are returned in the exact order posted.
    ->orderBy('c.cid', 'DESC')
517
518
519
520
521
    ->range(0, $number)
    ->execute()
    ->fetchAll();

  return $comments ? $comments : array();
522
523
}

524
525
/**
 * Calculate page number for first new comment.
526
527
528
529
530
531
532
533
534
 *
 * @param $num_comments
 *   Number of comments.
 * @param $new_replies
 *   Number of new replies.
 * @param $node
 *   The first new comment node.
 * @return
 *   "page=X" if the page number is greater than zero; empty string otherwise.
535
 */
536
function comment_new_page_count($num_comments, $new_replies, $node) {
537
538
  $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
  $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
539
  $pagenum = NULL;
540
  $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
541
542
  if ($num_comments <= $comments_per_page) {
    // Only one page of comments.
543
    $pageno = 0;
544
  }
545
546
547
  elseif ($flat) {
    // Flat comments.
    $count = $num_comments - $new_replies;
548
    $pageno = $count / $comments_per_page;
549
  }
550
  else {
551
552
553
554
555
556
557
558
    // 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)
559
560
      ->orderBy('created', 'DESC')
      ->orderBy('cid', 'DESC')
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
      ->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,
577
      ':nid' => $node->nid,
578
      ':thread' => $first_thread,
579
    ))->fetchField();
580

581
    $pageno = $count / $comments_per_page;
582
  }
583

584
  if ($pageno >= 1) {
585
    $pagenum = array('page' => intval($pageno));
586
  }
587

588
589
590
  return $pagenum;
}

591
/**
592
 * Returns HTML for a list of recent comments to be displayed in the comment block.
593
594
595
 *
 * @ingroup themeable
 */
596
597
function theme_comment_block() {
  $items = array();
598
599
  $number = variable_get('comment_block_count', 10);
  foreach (comment_get_recent($number) as $comment) {
600
    $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>';
601
  }
602

603
  if ($items) {
604
    return theme('item_list', array('items' => $items));
605
  }
606
607
608
  else {
    return t('No comments available.');
  }
609
610
}

Dries's avatar
   
Dries committed
611
/**
612
 * Implements hook_node_view().
Dries's avatar
   
Dries committed
613
 */
614
function comment_node_view($node, $view_mode) {
Dries's avatar
   
Dries committed
615
616
  $links = array();

617
  if ($node->comment != COMMENT_NODE_HIDDEN) {
618
    if ($view_mode == 'rss') {
619
620
621
622
623
      // 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))
      );
624
    }
625
    elseif ($view_mode == 'teaser') {
626
627
628
      // 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
629
      if (user_access('access comments')) {
630
        if (!empty($node->comment_count)) {
631
          $links['comment-comments'] = array(
632
            'title' => format_plural($node->comment_count, '1 comment', '@count comments'),
633
634
            'href' => "node/$node->nid",
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
635
636
            'fragment' => 'comments',
            'html' => TRUE,
637
          );
638
639
          // Show a link to the first new comment.
          if ($new = comment_num_new($node->nid)) {
640
            $links['comment-new-comments'] = array(
641
              'title' => format_plural($new, '1 new comment', '@count new comments'),
642
              'href' => "node/$node->nid",
643
              'query' => comment_new_page_count($node->comment_count, $new, $node),
644
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
645
646
              'fragment' => 'new',
              'html' => TRUE,
647
            );
Dries's avatar
   
Dries committed
648
649
          }
        }
650
651
652
653
654
655
656
657
658
659
      }
      if ($node->comment == COMMENT_NODE_OPEN) {
        if (user_access('post comments')) {
          $links['comment-add'] = array(
            'title' => t('Add new comment'),
            'href' => "comment/reply/$node->nid",
            'attributes' => array('title' => t('Add a new comment to this page.')),
            'fragment' => 'comment-form',
          );
        }
Dries's avatar
   
Dries committed
660
        else {
661
          $links['comment-forbidden'] = array(
662
663
664
            'title' => theme('comment_post_forbidden', array('node' => $node)),
            'html' => TRUE,
          );
Dries's avatar
   
Dries committed
665
666
667
        }
      }
    }
668
    elseif ($view_mode != 'search_index' && $view_mode != 'search_result') {
669
670
671
      // 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
672
      // indexing or constructing a search result excerpt.
673
      if ($node->comment == COMMENT_NODE_OPEN) {
674
        $comment_form_location = variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW);
Dries's avatar
   
Dries committed
675
        if (user_access('post comments')) {
676
677
678
679
680
681
682
683
684
685
686
687
          // 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";
            }
688
          }
Dries's avatar
   
Dries committed
689
690
        }
        else {
691
          $links['comment-forbidden'] = array(
692
693
694
            'title' => theme('comment_post_forbidden', array('node' => $node)),
            'html' => TRUE,
          );
Dries's avatar
   
Dries committed
695
696
697
        }
      }
    }
Dries's avatar
Dries committed
698

699
700
701
702
703
    $node->content['links']['comment'] = array(
      '#theme' => 'links__node__comment',
      '#links' => $links,
      '#attributes' => array('class' => array('links', 'inline')),
    );
Dries's avatar
Dries committed
704

705
706
707
    // 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
708
    // displayed in 'full' view mode within another node.
709
    if ($node->comment && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) {
710
      $node->content['comments'] = comment_node_page_additions($node);
711
    }
712
  }
Dries's avatar
   
Dries committed
713
714
}

715
716
717
718
719
720
/**
 * Build the comment-related elements for node detail pages.
 *
 * @param $node
 *  A node object.
 */
721
function comment_node_page_additions($node) {
722
723
724
725
726
  $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.
727
  if (($node->comment_count && user_access('access comments')) || user_access('administer comments')) {
728
729
730
    $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)) {
731
732
      $comments = comment_load_multiple($cids);
      comment_prepare_thread($comments);
733
      $build = comment_view_multiple($comments, $node);
734
735
736
737
738
739
740
      $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)) {
741
    $build = drupal_get_form("comment_node_{$node->type}_form", (object) array('nid' => $node->nid));
742
743
744
745
746
    $additions['comment_form'] = $build;
  }

  if ($additions) {
    $additions += array(
747
      '#theme' => 'comment_wrapper__node_' . $node->type,
748
749
750
751
752
753
754
755
756
757
      '#node' => $node,
      'comments' => array(),
      'comment_form' => array(),
    );
  }

  return $additions;
}

/**
758
 * Retrieve comments for a thread.
759
760
761
 *
 * @param $node
 *   The node whose comment(s) needs rendering.
762
763
764
765
 * @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.
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
816
817
818
819
820
 *
 * 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.
 */
821
function comment_get_thread($node, $mode, $comments_per_page) {
822
823
824
825
826
  $query = db_select('comment', 'c')->extend('PagerDefault');
  $query->addField('c', 'cid');
  $query
    ->condition('c.nid', $node->nid)
    ->addTag('node_access')
827
828
    ->addTag('comment_filter')
    ->addMetaData('node', $node)
829
830
831
832
833
834
    ->limit($comments_per_page);

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

  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.
850
851
    $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
    $query->orderBy('torder', 'ASC');
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
  }

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

  return $cids;
}

/**
 * Loop over comment thread, noting indentation level.
 *
 * @param array $comments
 *   An array of comment objects, keyed by cid.
 * @return
 *   The $comments argument is altered by reference with indentation information.
 */
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;
}

/**
 * Generate an array for rendering the given comment.
 *
 * @param $comment
 *   A comment object.
908
909
 * @param $node
 *   The node the comment is attached to.
910
911
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
912
913
914
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
915
916
917
918
 *
 * @return
 *   An array as expected by drupal_render().
 */
919
920
921
922
923
function comment_view($comment, $node, $view_mode = 'full', $langcode = NULL) {
  if (!isset($langcode)) {
    $langcode = $GLOBALS['language_content']->language;
  }

924
  // Populate $comment->content with a render() array.
925
  comment_build_content($comment, $node, $view_mode, $langcode);
926
927

  $build = $comment->content;
928
929
  // We don't need duplicate rendering info in comment->content.
  unset($comment->content);
930
931

  $build += array(
932
    '#theme' => 'comment__node_' . $node->type,
933
    '#comment' => $comment,
934
    '#node' => $node,
935
    '#view_mode' => $view_mode,
936
    '#language' => $langcode,
937
938
  );

939
940
941
  if (empty($comment->in_preview)) {
    $prefix = '';
    $is_threaded = isset($comment->divs) && variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED) == COMMENT_MODE_THREADED;
942

943
944
945
946
    // Add 'new' anchor if needed.
    if (!empty($comment->first_new)) {
      $prefix .= "<a id=\"new\"></a>\n";
    }
947

948
949
950
951
    // 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">';
    }
952

953
954
955
    // Add anchor for each comment.
    $prefix .= "<a id=\"comment-$comment->cid\"></a>\n";
    $build['#prefix'] = $prefix;
956

957
958
959
960
    // Close all open divs.
    if ($is_threaded && !empty($comment->divs_final)) {
      $build['#suffix'] = str_repeat('</div>', $comment->divs_final);
    }
961
962
  }

963
  // Allow modules to modify the structured comment.
964
965
  $type = 'comment';
  drupal_alter(array('comment_view', 'entity_view'), $build, $type);
966

967
968
969
970
971
972
973
  return $build;
}

/**
 * Builds a structured array representing the comment's content.
 *
 * The content built for the comment (field values, comments, file attachments or
974
 * other comment components) will vary depending on the $view_mode parameter.
975
976
977
 *
 * @param $comment
 *   A comment object.
978
979
 * @param $node
 *   The node the comment is attached to.
980
981
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
982
983
984
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
985
 */
986
987
988
989
990
function comment_build_content($comment, $node, $view_mode = 'full', $langcode = NULL) {
  if (!isset($langcode)) {
    $langcode = $GLOBALS['language_content']->language;
  }

991
992
  // Remove previously built content, if exists.
  $comment->content = array();
993

994
  // Build fields content.
995
996
  field_attach_prepare_view('comment', array($comment->cid => $comment), $view_mode, $langcode);
  entity_prepare_view('comment', array($comment->cid => $comment), $langcode);
997
  $comment->content += field_attach_view('comment', $comment, $view_mode, $langcode);
998

999
1000
1001
1002
1003
  $comment->content['links'] = array(
    '#theme' => 'links__comment',
    '#pre_render' => array('drupal_pre_render_links'),
    '#attributes' => array('class' => array('links', 'inline')),
  );
1004
1005
  if (empty($comment->in_preview)) {
    $comment->content['links']['comment'] = array(
1006
      '#theme' => 'links__comment__comment',
1007
      '#links' => comment_links($comment, $node),
1008
      '#attributes' => array('class' => array('links', 'inline')),
1009
1010
1011
1012
    );
  }

  // Allow modules to make their own additions to the comment.
1013
  module_invoke_all('comment_view', $comment, $view_mode, $langcode);
1014
  module_invoke_all('entity_view', $comment, 'comment', $view_mode, $langcode);
1015
1016
}

1017
1018
1019
1020
1021
1022
1023
/**
 * Helper function, build links for an individual comment.
 *
 * Adds reply, edit, delete etc. depending on the current user permissions.
 *
 * @param $comment
 *   The comment object.
1024
1025
 * @param $node
 *   The node the comment is attached to.
1026
1027
1028
 * @return
 *   A structured array of links.
 */
1029
function comment_links($comment, $node) {
1030
1031
1032
  $links = array();
  if ($node->comment == COMMENT_NODE_OPEN) {
    if (user_access('administer comments') && user_access('post comments')) {
1033
      $links['comment-delete'] = array(
1034
        'title' => t('delete'),
1035
        'href' => "comment/$comment->cid/delete",
1036
1037
        'html' => TRUE,
      );
1038
      $links['comment-edit'] = array(
1039
        'title' => t('edit'),
1040
        'href' => "comment/$comment->cid/edit",