comment.module 82.3 KB
Newer Older
1
<?php
2
// $Id$
Dries's avatar
 
Dries committed
3

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

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

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

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

/**
 * Comments are displayed as a threaded list - expanded.
 */
31
define('COMMENT_MODE_THREADED', 1);
Dries's avatar
Dries committed
32
33

/**
34
 * Anonymous posters cannot enter their contact information.
Dries's avatar
Dries committed
35
36
 */
define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0);
37
38
39
40

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

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

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

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

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

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

/**
69
 * Comments for this node are open.
70
 */
71
define('COMMENT_NODE_OPEN', 2);
72

73
/**
74
 * Implement hook_help().
75
 */
76
77
function comment_help($path, $arg) {
  switch ($path) {
Dries's avatar
   
Dries committed
78
    case 'admin/help#comment':
79
      $output  = '<p>' . t('The comment module allows visitors to comment on your posts, creating ad hoc discussion boards. Any <a href="@content-type">content type</a> may have its <em>Default comment setting</em> set to <em>Open</em> to allow comments, <em>Hidden</em> to hide existing comments and prevent new comments or <em>Closed</em> to allow existing comments to be viewed but no new comments added. Comment display settings and other controls may also be customized for each content type.', array('@content-type' => url('admin/structure/types'))) . '</p>';
80
      $output .= '<p>' . t('Comment permissions are assigned to user roles, and are used to determine whether anonymous users (or other roles) are allowed to comment on posts. If anonymous users are allowed to comment, their individual contact information may be retained in cookies stored on their local computer for use in later comment submissions. When a comment has no replies, it may be (optionally) edited by its author. The comment module uses the same text formats and HTML tags available when creating other forms of content.') . '</p>';
81
      $output .= '<p>' . t('Change comment settings on the content type\'s <a href="@content-type">edit page</a>.', array('@content-type' => url('admin/structure/types'))) . '</p>';
82
      $output .= '<p>' . t('For more information, see the online handbook entry for <a href="@comment">Comment module</a>.', array('@comment' => 'http://drupal.org/handbook/modules/comment/')) . '</p>';
83

84
      return $output;
85
  }
Dries's avatar
   
Dries committed
86
87
}

88
/**
89
 * Implement hook_entity_info().
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
 */
function comment_entity_info() {
  $return =  array(
    'comment' => array(
      'label' => t('Comment'),
      'base table' => 'comment',
      'fieldable' => TRUE,
      'controller class' => 'CommentController',
      'object keys' => array(
        'id' => 'cid',
        'bundle' => 'node_type',
      ),
      'bundle keys' => array(
        'bundle' => 'type',
      ),
      'bundles' => array(),
      'static cache' => FALSE,
    ),
  );

  foreach (node_type_get_names() as $type => $name) {
    $return['comment']['bundles']['comment_node_' . $type] = array(
      'label' => $name,
    );
  }

  return $return;
}

119
/**
120
 * Implement hook_theme().
121
122
123
124
 */
function comment_theme() {
  return array(
    'comment_block' => array(
125
      'variables' => array(),
126
127
    ),
    'comment_preview' => array(
128
      'variables' => array('comment' => NULL),
129
130
    ),
    'comment' => array(
131
      'template' => 'comment',
132
      'render element' => 'elements',
133
134
    ),
    'comment_post_forbidden' => array(
135
      'variables' => array('node' => NULL),
136
137
    ),
    'comment_wrapper' => array(
138
      'template' => 'comment-wrapper',
139
      'render element' => 'content',
140
141
142
143
    ),
  );
}

Dries's avatar
   
Dries committed
144
/**
145
 * Implement hook_menu().
Dries's avatar
   
Dries committed
146
 */
147
function comment_menu() {
148
  $items['admin/content/comment'] = array(
149
    'title' => 'Comments',
150
    'description' => 'List and edit site comments and the comment approval queue.',
151
152
    'page callback' => 'comment_admin',
    'access arguments' => array('administer comments'),
153
    'type' => MENU_LOCAL_TASK,
154
    'file' => 'comment.admin.inc',
155
  );
156
  // Tabs begin here.
157
  $items['admin/content/comment/new'] = array(
158
    'title' => 'Published comments',
159
160
161
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
162
  $items['admin/content/comment/approval'] = array(
163
164
    'title' => 'Unapproved comments',
    'title callback' => 'comment_count_unpublished',
165
    'page arguments' => array('approval'),
166
    'access arguments' => array('administer comments'),
167
168
    'type' => MENU_LOCAL_TASK,
  );
169
170
171
172
173
  $items['comment/%comment'] = array(
    'title' => 'Comment permalink',
    'page callback' => 'comment_permalink',
    'page arguments' => array(1),
    'access arguments' => array('access comments'),
174
175
    'type' => MENU_CALLBACK,
  );
176
177
178
179
180
181
182
  $items['comment/%comment/view'] = array(
    'title' => 'View comment',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['comment/%comment/edit'] = array(
    'title' => 'Edit',
183
    'page callback' => 'drupal_get_form',
184
    'page arguments' => array('comment_form', 1),
185
    'access callback' => 'comment_access',
186
187
    'access arguments' => array('edit', 1),
    'type' => MENU_LOCAL_TASK,
188
    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
189
190
191
192
193
194
195
196
    'weight' => 0,
  );
  $items['comment/%comment/approve'] = array(
    'title' => 'Approve',
    'page callback' => 'comment_approve',
    'page arguments' => array(1),
    'access arguments' => array('administer comments'),
    'type' => MENU_LOCAL_TASK,
197
    'context' => MENU_CONTEXT_INLINE,
198
199
200
201
202
203
204
205
206
    'file' => 'comment.pages.inc',
    'weight' => 1,
  );
  $items['comment/%comment/delete'] = array(
    'title' => 'Delete',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('comment_confirm_delete', 1),
    'access arguments' => array('administer comments'),
    'type' => MENU_LOCAL_TASK,
207
    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
208
209
    'file' => 'comment.admin.inc',
    'weight' => 2,
210
  );
211
  $items['comment/reply/%node'] = array(
212
    'title' => 'Add new comment',
213
    'page callback' => 'comment_reply',
214
    'page arguments' => array(2),
215
216
217
    'access callback' => 'node_access',
    'access arguments' => array('view', 2),
    'type' => MENU_CALLBACK,
218
    'file' => 'comment.pages.inc',
219
  );
Dries's avatar
   
Dries committed
220
221
222
223

  return $items;
}

224
225
226
227
228
229
230
231
232
233
/**
 * 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));
}

234
/**
235
 * Implement hook_node_type_insert().
236
 */
237
function comment_node_type_insert($info) {
238
  field_attach_create_bundle('comment', 'comment_node_' . $info->type);
239
}
240

241
242
243
244
245
/**
 * Implement hook_node_type_update().
 */
function comment_node_type_update($info) {
  if (!empty($info->old_type) && $info->type != $info->old_type) {
246
    field_attach_rename_bundle('comment', 'comment_node_' . $info->old_type, 'comment_node_' . $info->type);
247
248
  }
}
249

250
251
252
253
/**
 * Implement hook_node_type_delete().
 */
function comment_node_type_delete($info) {
254
  field_attach_delete_bundle('comment', 'comment_node_' . $info->type);
255
256
257
258
259
260
261
262
263
264
265
  $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);
266
267
268
  }
}

Dries's avatar
   
Dries committed
269
/**
270
 * Implement hook_permission().
Dries's avatar
   
Dries committed
271
 */
272
function comment_permission() {
273
  return array(
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
    'administer comments' => array(
      'title' => t('Administer comments'),
      'description' => t('Manage and approve comments, and configure comment administration settings.'),
    ),
    'access comments' => array(
      'title' => t('Access comments'),
      'description' => t('View comments attached to content.'),
    ),
    'post comments' => array(
      'title' => t('Post comments'),
      'description' => t('Add comments to content (approval required).'),
    ),
    'post comments without approval' => array(
      'title' => t('Post comments without approval'),
      'description' => t('Add comments to content (no approval required).'),
    ),
290
  );
Dries's avatar
   
Dries committed
291
292
293
}

/**
294
 * Implement hook_block_info().
Dries's avatar
   
Dries committed
295
 */
296
function comment_block_info() {
297
  $blocks['recent']['info'] = t('Recent comments');
298

299
300
  return $blocks;
}
301

302
/**
303
 * Implement hook_block_configure().
304
305
306
307
308
309
310
311
 */
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)),
  );
312

313
314
  return $form;
}
315

316
/**
317
 * Implement hook_block_save().
318
319
320
321
 */
function comment_block_save($delta = '', $edit = array()) {
  variable_set('comment_block_count', (int)$edit['comment_block_count']);
}
322

323
/**
324
 * Implement hook_block_view().
325
326
327
328
329
330
331
 *
 * 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');
332

333
    return $block;
Dries's avatar
   
Dries committed
334
335
336
  }
}

337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
/**
 * 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.
 *
 * @param $comment
 *   A comment object.
 * @return
 *   The comment listing set to the page on which the comment appears.
 */
function comment_permalink($comment) {
  $node = node_load($comment->nid);
  if ($node && $comment) {

    // 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.
366
    return menu_execute_active_handler('node/' . $node->nid, FALSE);
367
368
369
370
  }
  drupal_not_found();
}

371
/**
372
373
374
375
376
377
378
 * Find the most recent comments that are available to the current user.
 *
 * This is done in two steps:
 *   1. Query the {node_comment_statistics} table to find n number of nodes that
 *      have the most recent comments. This table is indexed on
 *      last_comment_timestamp, thus making it a fast query.
 *   2. Load the information from the comments table based on the nids found
379
380
 *      in step 1.
 *
381
 * @param integer $number
382
383
 *   (optional) The maximum number of comments to find.
 * @return
384
385
386
 *   An array of comment objects each containing a nid, subject, cid, created
 *   and changed, or an empty array if there are no recent comments visible
 *   to the current user.
387
388
 */
function comment_get_recent($number = 10) {
389
390
  // Step 1: Select a $number of nodes which have new comments,
  //         and are visible to the current user.
391
  $nids = db_query_range("SELECT nc.nid FROM {node_comment_statistics} nc WHERE nc.comment_count > 0 ORDER BY nc.last_comment_timestamp DESC", 0, $number)->fetchCol();
392
393
394

  $comments = array();
  if (!empty($nids)) {
395
396
    // Step 2: From among the comments on the nodes selected in the first query,
    //         find the $number of most recent comments.
397
    // Using Query Builder here for the IN-Statement.
398
399
    $query = db_select('comment', 'c');
    $query->innerJoin('node', 'n', 'n.nid = c.nid');
400
    return $query
401
      ->fields('c', array('nid', 'subject', 'cid', 'created', 'changed'))
402
403
404
405
406
407
408
      ->condition('c.nid', $nids, 'IN')
      ->condition('c.status', COMMENT_PUBLISHED)
      ->condition('n.status', 1)
      ->orderBy('c.cid', 'DESC')
      ->range(0, $number)
      ->execute()
      ->fetchAll();
409
410
411
412
413
  }

  return $comments;
}

414
415
/**
 * Calculate page number for first new comment.
416
417
418
419
420
421
422
423
424
 *
 * @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.
425
 */
426
function comment_new_page_count($num_comments, $new_replies, stdClass $node) {
427
428
  $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
  $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
429
  $pagenum = NULL;
430
  $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
431
432
  if ($num_comments <= $comments_per_page) {
    // Only one page of comments.
433
    $pageno = 0;
434
  }
435
436
437
438
439
  elseif ($flat) {
    // Flat comments.
    $count = $num_comments - $new_replies;
    $pageno =  $count / $comments_per_page;
  }
440
  else {
441
442
    // Threaded comments.
    // Find the first thread with a new comment.
443
    $result = db_query_range('SELECT thread FROM (SELECT thread
444
      FROM {comment}
445
446
      WHERE nid = :nid
        AND status = 0
447
      ORDER BY changed DESC) AS thread
448
      ORDER BY SUBSTRING(thread, 1, (LENGTH(thread) - 1))', 0, $new_replies, array(':nid' => $node->nid))->fetchField();
449
    $thread = substr($result, 0, -1);
450
    $count = db_query('SELECT COUNT(*) FROM {comment} WHERE nid = :nid AND status = 0 AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread', array(
451
      ':nid' => $node->nid,
452
453
      ':thread' => $thread,
    ))->fetchField();
454
455
    $pageno =  $count / $comments_per_page;
  }
456

457
  if ($pageno >= 1) {
458
    $pagenum = array('page' => intval($pageno));
459
  }
460

461
462
463
  return $pagenum;
}

464
/**
465
 * Returns a formatted list of recent comments to be displayed in the comment block.
466
 *
467
468
 * @return
 *   The comment list HTML.
469
470
 * @ingroup themeable
 */
471
472
function theme_comment_block() {
  $items = array();
473
474
  $number = variable_get('comment_block_count', 10);
  foreach (comment_get_recent($number) as $comment) {
475
    $items[] = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)) . '<br />' . t('@time ago', array('@time' => format_interval(REQUEST_TIME - $comment->changed)));
476
  }
477

478
  if ($items) {
479
    return theme('item_list', array('items' => $items));
480
  }
481
482
}

Dries's avatar
   
Dries committed
483
/**
484
 * Implement hook_node_view().
Dries's avatar
   
Dries committed
485
 */
486
function comment_node_view(stdClass $node, $build_mode) {
Dries's avatar
   
Dries committed
487
488
  $links = array();

489
  if ($node->comment) {
490
    if ($build_mode == 'rss') {
491
492
493
494
495
496
497
      if ($node->comment != COMMENT_NODE_HIDDEN) {
        // 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))
        );
      }
498
    }
499
    elseif ($build_mode == 'teaser') {
Dries's avatar
   
Dries committed
500
501
      // Main page: display the number of comments that have been posted.
      if (user_access('access comments')) {
502
        if (!empty($node->comment_count)) {
503
          $links['comment_comments'] = array(
504
            'title' => format_plural($node->comment_count, '1 comment', '@count comments'),
505
506
            'href' => "node/$node->nid",
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
507
508
            'fragment' => 'comments',
            'html' => TRUE,
509
          );
510
511

          $new = comment_num_new($node->nid);
Dries's avatar
   
Dries committed
512
          if ($new) {
513
            $links['comment_new_comments'] = array(
514
              'title' => format_plural($new, '1 new comment', '@count new comments'),
515
              'href' => "node/$node->nid",
516
              'query' => comment_new_page_count($node->comment_count, $new, $node),
517
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
518
519
              'fragment' => 'new',
              'html' => TRUE,
520
            );
Dries's avatar
   
Dries committed
521
522
523
          }
        }
        else {
524
          if ($node->comment == COMMENT_NODE_OPEN) {
Dries's avatar
   
Dries committed
525
            if (user_access('post comments')) {
526
              $links['comment_add'] = array(
527
                'title' => t('Add new comment'),
528
529
                'href' => "comment/reply/$node->nid",
                'attributes' => array('title' => t('Add a new comment to this page.')),
530
531
                'fragment' => 'comment-form',
                'html' => TRUE,
532
              );
Dries's avatar
   
Dries committed
533
534
            }
            else {
535
              $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node));
Dries's avatar
   
Dries committed
536
537
538
539
540
541
            }
          }
        }
      }
    }
    else {
542
543
      // Node page: add a "post comment" link if the user is allowed to post
      // comments and if this node is not read-only.
544
      if ($node->comment == COMMENT_NODE_OPEN) {
Dries's avatar
   
Dries committed
545
        if (user_access('post comments')) {
546
547
548
549
550
551
          $links['comment_add'] = array(
            'title' => t('Add new comment'),
            'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
            'fragment' => 'comment-form',
            'html' => TRUE,
          );
552
          if (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_SEPARATE_PAGE) {
553
554
555
556
            $links['comment_add']['href'] = "comment/reply/$node->nid";
          }
          else {
            $links['comment_add']['href'] = "node/$node->nid";
557
          }
Dries's avatar
   
Dries committed
558
559
        }
        else {
560
          $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node));
Dries's avatar
   
Dries committed
561
562
563
        }
      }
    }
Dries's avatar
Dries committed
564

565
566
567
    if (isset($links['comment_forbidden'])) {
      $links['comment_forbidden']['html'] = TRUE;
    }
568

569
    $node->content['links']['comment'] = array(
570
571
      '#theme' => 'links',
      '#links' => $links,
572
      '#attributes' => array('class' => array('links', 'inline')),
573
    );
Dries's avatar
Dries committed
574

575
576
577
578
579
580
581
    // 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
    // displayed in 'full' build mode within another node.
    $page_node = menu_get_object();
    if ($node->comment && isset($page_node->nid) && $page_node->nid == $node->nid && empty($node->in_preview) && user_access('access comments')) {
      $node->content['comments'] = comment_node_page_additions($node);
582
    }
583
  }
Dries's avatar
   
Dries committed
584
585
}

586
587
588
589
590
591
/**
 * Build the comment-related elements for node detail pages.
 *
 * @param $node
 *  A node object.
 */
592
function comment_node_page_additions(stdClass $node) {
593
594
595
596
597
598
  $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.
  if ($node->comment_count || user_access('administer comments')) {
599
600
601
    $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)) {
602
603
      $comments = comment_load_multiple($cids);
      comment_prepare_thread($comments);
604
      $build = comment_build_multiple($comments, $node);
605
      $build['#attached']['css'][] = drupal_get_path('module', 'comment') . '/comment.css';
606
607
608
609
610
611
612
      $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)) {
613
    $build = drupal_get_form('comment_form', (object) array('nid' => $node->nid));
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
    $additions['comment_form'] = $build;
  }

  if ($additions) {
    $additions += array(
      '#theme' => 'comment_wrapper',
      '#node' => $node,
      'comments' => array(),
      'comment_form' => array(),
    );
  }

  return $additions;
}

/**
630
 * Retrieve comments for a thread.
631
632
633
 *
 * @param $node
 *   The node whose comment(s) needs rendering.
634
635
636
637
 * @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.
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
 *
 * 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.
 */
693
function comment_get_thread(stdClass $node, $mode, $comments_per_page) {
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
  $query = db_select('comment', 'c')->extend('PagerDefault');
  $query->addField('c', 'cid');
  $query
    ->condition('c.nid', $node->nid)
    ->addTag('node_access')
    ->limit($comments_per_page);

  $count_query = db_select('comment', 'c');
  $count_query->addExpression('COUNT(*)');
  $count_query
    ->condition('c.nid', $node->nid)
    ->addTag('node_access');

  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.
    $query->orderBy('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'ASC');
  }

  $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.
775
776
 * @param $node
 *   The node the comment is attached to.
777
778
779
780
781
782
 * @param $build_mode
 *   Build mode, e.g. 'full', 'teaser'...
 *
 * @return
 *   An array as expected by drupal_render().
 */
783
function comment_build($comment, stdClass $node, $build_mode = 'full') {
784
  // Populate $comment->content with a render() array.
785
  comment_build_content($comment, $node, $build_mode);
786
787

  $build = $comment->content;
788
789
  // We don't need duplicate rendering info in comment->content.
  unset($comment->content);
790
791
792
793

  $build += array(
    '#theme' => 'comment',
    '#comment' => $comment,
794
    '#node' => $node,
795
796
    '#build_mode' => $build_mode,
  );
797
798
  // Add contextual links for this comment.
  $build['#contextual_links']['comment'] = menu_contextual_links('comment', array($comment->cid));
799
800

  $prefix = '';
801
  $is_threaded = isset($comment->divs) && variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED) == COMMENT_MODE_THREADED;
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

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

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

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

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

  return $build;
}

/**
 * Builds a structured array representing the comment's content.
 *
 * The content built for the comment (field values, comments, file attachments or
 * other comment components) will vary depending on the $build_mode parameter.
 *
 * @param $comment
 *   A comment object.
833
834
 * @param $node
 *   The node the comment is attached to.
835
836
837
 * @param $build_mode
 *   Build mode, e.g. 'full', 'teaser'...
 */
838
function comment_build_content($comment, stdClass $node, $build_mode = 'full') {
839
840
  // Remove previously built content, if exists.
  $comment->content = array();
841
842
843

  // Build comment body.
  $comment->content['comment_body'] = array(
844
    '#markup' => check_markup($comment->comment, $comment->format, '', TRUE),
845
846
  );

847
  field_attach_prepare_view('comment', array($comment->cid => $comment), $build_mode);
848
849
  $comment->content += field_attach_view('comment', $comment, $build_mode);

850
851
852
  if (empty($comment->in_preview)) {
    $comment->content['links']['comment'] = array(
      '#theme' => 'links',
853
      '#links' => comment_links($comment, $node),
854
      '#attributes' => array('class' => array('links', 'inline')),
855
856
857
858
859
860
861
862
863
864
    );
  }

  // Allow modules to make their own additions to the comment.
  module_invoke_all('comment_view', $comment, $build_mode);

  // Allow modules to modify the structured comment.
  drupal_alter('comment_build', $comment, $build_mode);
}

865
866
867
868
869
870
871
/**
 * Helper function, build links for an individual comment.
 *
 * Adds reply, edit, delete etc. depending on the current user permissions.
 *
 * @param $comment
 *   The comment object.
872
873
 * @param $node
 *   The node the comment is attached to.
874
875
876
 * @return
 *   A structured array of links.
 */
877
function comment_links($comment, stdClass $node) {
878
879
880
881
882
  $links = array();
  if ($node->comment == COMMENT_NODE_OPEN) {
    if (user_access('administer comments') && user_access('post comments')) {
      $links['comment_delete'] = array(
        'title' => t('delete'),
883
        'href' => "comment/$comment->cid/delete",
884
885
886
887
        'html' => TRUE,
      );
      $links['comment_edit'] = array(
        'title' => t('edit'),
888
        'href' => "comment/$comment->cid/edit",
889
890
891
892
893
894
895
896
897
898
        'html' => TRUE,
      );
      $links['comment_reply'] = array(
        'title' => t('reply'),
        'href' => "comment/reply/$comment->nid/$comment->cid",
        'html' => TRUE,
      );
      if ($comment->status == COMMENT_NOT_PUBLISHED) {
        $links['comment_approve'] = array(
          'title' => t('approve'),
899
          'href' => "comment/$comment->cid/approve",
900
901
902
903
904
905
906
907
          'html' => TRUE,
        );
      }
    }
    elseif (user_access('post comments')) {
      if (comment_access('edit', $comment)) {
        $links['comment_edit'] = array(
          'title' => t('edit'),
908
          'href' => "comment/$comment->cid/edit",
909
910
911
912
913
914
915
916
917
918
          'html' => TRUE,
        );
      }
      $links['comment_reply'] = array(
        'title' => t('reply'),
        'href' => "comment/reply/$comment->nid/$comment->cid",
        'html' => TRUE,
      );
    }
    else {
919
      $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node));
920
921
922
923
924
925
      $links['comment_forbidden']['html'] = TRUE;
    }
  }
  return $links;
}

926
927
928
929
930
/**
 * Construct a drupal_render() style array from an array of loaded comments.
 *
 * @param $comments
 *   An array of comments as returned by comment_load_multiple().
931
932
 * @param $node
 *   The node the comments are attached to.
933
934
935
936
937
938
939
 * @param $build_mode
 *   Build mode, e.g. 'full', 'teaser'...
 * @param $weight
 *   An integer representing the weight of the first comment in the list.
 * @return
 *   An array in the format expected by drupal_render().
 */
940
function comment_build_multiple($comments, stdClass $node, $build_mode = 'full', $weight = 0) {
941
942
  field_attach_prepare_view('comment', $comments, $build_mode);

943
944
945
946
  $build = array(
    '#sorted' => TRUE,
  );
  foreach ($comments as $comment) {
947
    $build[$comment->cid] = comment_build($comment, $node, $build_mode);
948
949
950
951
952
953
    $build[$comment->cid]['#weight'] = $weight;
    $weight++;
  }
  return $build;
}

954
/**
955
 * Implement hook_form_FORM_ID_alter().
956
 */
957
958
function comment_form_node_type_form_alter(&$form, $form_state) {
  if (isset($form['identity']['type'])) {
959
960
961
962
    $form['comment'] = array(
      '#type' => 'fieldset',
      '#title' => t('Comment settings'),
      '#collapsible' => TRUE,
963
      '#collapsed' => TRUE,
964
      '#group' => 'additional_settings',
965
966
967
      '#attached' => array(
        'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
      ),
968
969
    );
    $form['comment']['comment_default_mode'] = array(
970
971
972
973
      '#type' => 'checkbox',
      '#title' => t('Threading'),
      '#default_value' => variable_get('comment_default_mode_' . $form['#node_type']->type, COMMENT_MODE_THREADED),
      '#description' => t('Show comment replies in a threaded list.'),
974
975
976
    );
    $form['comment']['comment_default_per_page'] = array(
      '#type' => 'select',
977
      '#title' => t('Comments per page'),
978
      '#default_value' => variable_get('comment_default_per_page_' . $form['#node_type']->type, 50),
979
      '#options' => _comment_per_page(),
980
981
982
983
984
985
986
    );

    $form['comment']['comment'] = array(
      '#type' => 'select',
      '#title' => t('Default comment setting for new content'),
      '#default_value' => variable_get('comment_' . $form['#node_type']->type, COMMENT_NODE_OPEN),
      '#options' => array(t('Hidden'), t('Closed'), t('Open')),
987
988
    );
    $form['comment']['comment_anonymous'] = array(
989
      '#type' => 'select',
990
      '#title' => t('Anonymous commenting'),
991
      '#default_value' => variable_get('comment_anonymous_' . $form['#node_type']->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT),
992
993
994
      '#options' => array(
        COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'),
        COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'),
995
        COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information'))
996
    );
997

998
    if (!user_access('post comments', drupal_anonymous_user())) {
999
      $form['comment']['comment_anonymous']['#access'] = FALSE;
1000
    }
1001

1002
    $form['comment']['comment_subject_field'] = array(
1003
1004
      '#type' => 'checkbox',
      '#title' => t('Allow comment title'),
1005
      '#default_value' => variable_get('comment_subject_field_' . $form['#node_type']->type, 1),
1006
1007
    );
    $form['comment']['comment_form_location'] = array(
1008
1009
1010
1011
1012
      '#type' => 'checkbox',
      '#title' => t('Show reply form on the same page as comments'),
      '#default_value' => variable_get('comment_form_location_' . $form['#node_type']->type, COMMENT_FORM_BELOW),
    );
    $form['comment']['comment_preview'] = array(
1013
1014
1015
1016
1017
1018
1019
1020
      '#type' => 'radios',
      '#title' => t('Preview comment'),
      '#default_value' => variable_get('comment_preview_' . $form['#node_type']->type, DRUPAL_OPTIONAL),
      '#options' => array(
        DRUPAL_DISABLED => t('Disabled'),
        DRUPAL_OPTIONAL => t('Optional'),
        DRUPAL_REQUIRED => t('Required'),
      ),
1021
    );
1022
  }
1023
1024
1025
}

/**
1026
 * Implement hook_form_alter().
1027
1028
1029
 */
function comment_form_alter(&$form, $form_state, $form_id) {
  if (!empty($form['#node_edit_form'])) {
1030
1031
1032
1033
1034
1035
1036
    $node = $form['#node'];
    $form['comment_settings'] = array(
      '#type' => 'fieldset',
      '#access' => user_access('administer comments'),
      '#title' => t('Comment settings'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
1037
      '#group' => 'additional_settings',
1038
1039
1040
      '#attached' => array(
        'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
       ),
1041
1042
      '#weight' => 30,
    );
1043
1044
    $comment_count = isset($node->nid) ? db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array(':nid' => $node->nid))->fetchField() : 0;
    $comment_settings = ($node->comment == COMMENT_NODE_HIDDEN && empty($comment_count)) ? COMMENT_NODE_CLOSED : $node->comment;
1045
1046
1047
    $form['comment_settings']['comment'] = array(
      '#type' => 'radios',
      '#parents' => array('comment'),
1048
1049
1050
1051
1052
1053
1054
1055
1056
      '#default_value' => $comment_settings,
      '#options' => array(
        COMMENT_NODE_OPEN => t('Open'),
        COMMENT_NODE_CLOSED => t('Closed'),
        COMMENT_NODE_HIDDEN => t('Hidden'),
      ),
      COMMENT_NODE_OPEN => array(
        '#type' => 'radio',
        '#title' => t('Open'),
1057
        '#description' => t('Users with the "Post comments" permission can post comments.'),
1058
1059
1060
1061
1062
1063
1064
1065
        '#return_value' => COMMENT_NODE_OPEN,
        '#default_value' => $comment_settings,
        '#id' => 'edit-comment-2',
        '#parents' => array('comment'),
      ),
      COMMENT_NODE_CLOSED => array(
        '#type' => 'radio',
        '#title' => t('Closed'),
1066
        '#description' => t('Users cannot post comments, but existing comments will be displayed.'),
1067
1068
1069
1070
1071
1072
1073
1074
        '#return_value' => COMMENT_NODE_CLOSED,
        '#default_value' => $comment_settings,
        '#id' => 'edit-comment-1',
        '#parents' => array('comment'),
      ),
      COMMENT_NODE_HIDDEN => array(
        '#type' => 'radio',
        '#title' => t('Hidden'),
1075
        '#description' => t('Comments are hidden from view.'),
1076
1077
1078
1079
1080
        '#return_value' => COMMENT_NODE_HIDDEN,
        '#default_value' => $comment_settings,
        '#id' => 'edit-comment-0',
        '#parents' => array('comment'),
      ),
1081
    );
1082
1083
1084
1085
1086
    // If the node doesn't have any comments, the "hidden" option makes no
    // sense, so don't even bother presenting it to the user.
    if (empty($comment_count)) {
      unset($form['comment_settings']['comment']['#options'][COMMENT_NODE_HIDDEN]);
      unset($form['comment_settings']['comment'][COMMENT_NODE_HIDDEN]);
1087
      $form['comment_settings']['comment'][COMMENT_NODE_CLOSED]['#description'] = t('Users cannot post comments.');
1088
    }
1089
1090
1091
  }
}

Dries's avatar
   
Dries committed
1092
/**
1093
 * Implement hook_node_load().
Dries's avatar
   
Dries committed
1094
 */