comment.module 72.7 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 published.
15
 */
Dries's avatar
Dries committed
16
define('COMMENT_PUBLISHED', 0);
17
18
19
20

/**
 * Comment is awaiting approval.
 */
Dries's avatar
Dries committed
21
22
23
define('COMMENT_NOT_PUBLISHED', 1);

/**
24
 * Comments are displayed in a flat list - collapsed.
Dries's avatar
Dries committed
25
 */
26
define('COMMENT_MODE_FLAT_COLLAPSED', 1);
27
28
29
30

/**
 * Comments are displayed in a flat list - expanded.
 */
31
define('COMMENT_MODE_FLAT_EXPANDED', 2);
32
33
34
35

/**
 * Comments are displayed as a threaded list - collapsed.
 */
36
define('COMMENT_MODE_THREADED_COLLAPSED', 3);
37
38
39
40

/**
 * Comments are displayed as a threaded list - expanded.
 */
41
define('COMMENT_MODE_THREADED_EXPANDED', 4);
Dries's avatar
Dries committed
42
43

/**
44
 * Comments are ordered by date - newest first.
Dries's avatar
Dries committed
45
 */
46
define('COMMENT_ORDER_NEWEST_FIRST', 1);
47
48
49
50

/**
 * Comments are ordered by date - oldest first.
 */
51
define('COMMENT_ORDER_OLDEST_FIRST', 2);
Dries's avatar
Dries committed
52
53

/**
54
 * Comment controls should be shown above the comment list.
Dries's avatar
Dries committed
55
56
 */
define('COMMENT_CONTROLS_ABOVE', 0);
57
58
59
60

/**
 * Comment controls should be shown below the comment list.
 */
Dries's avatar
Dries committed
61
define('COMMENT_CONTROLS_BELOW', 1);
62
63
64
65

/**
 * Comment controls should be shown both above and below the comment list.
 */
Dries's avatar
Dries committed
66
define('COMMENT_CONTROLS_ABOVE_BELOW', 2);
67
68
69
70

/**
 * Comment controls are hidden.
 */
Dries's avatar
Dries committed
71
72
73
define('COMMENT_CONTROLS_HIDDEN', 3);

/**
74
 * Anonymous posters may not enter their contact information.
Dries's avatar
Dries committed
75
76
 */
define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0);
77
78
79
80

/**
 * Anonymous posters may leave their contact information.
 */
Dries's avatar
Dries committed
81
define('COMMENT_ANONYMOUS_MAY_CONTACT', 1);
82
83
84
85

/**
 * Anonymous posters must leave their contact information.
 */
Dries's avatar
Dries committed
86
87
88
define('COMMENT_ANONYMOUS_MUST_CONTACT', 2);

/**
89
 * Comment form should be displayed on a separate page.
Dries's avatar
Dries committed
90
91
 */
define('COMMENT_FORM_SEPARATE_PAGE', 0);
92
93
94
95

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

/**
99
 * Comments for this node are disabled.
Dries's avatar
Dries committed
100
101
 */
define('COMMENT_NODE_DISABLED', 0);
102
103
104
105

/**
 * Comments for this node are locked.
 */
Dries's avatar
Dries committed
106
define('COMMENT_NODE_READ_ONLY', 1);
107
108
109
110

/**
 * Comments are enabled on this node.
 */
Dries's avatar
Dries committed
111
define('COMMENT_NODE_READ_WRITE', 2);
112

113
/**
114
 * Comment preview is optional.
115
116
 */
define('COMMENT_PREVIEW_OPTIONAL', 0);
117
118
119
120

/**
 * Comment preview is required.
 */
121
122
define('COMMENT_PREVIEW_REQUIRED', 1);

123
124
125
/**
 * Implementation of hook_help().
 */
126
function comment_help($section) {
Dries's avatar
   
Dries committed
127
  switch ($section) {
Dries's avatar
   
Dries committed
128
    case 'admin/help#comment':
129
      $output = '<p>'. t('The comment module creates a discussion board for each post. Users can post comments to discuss a forum topic, weblog post, story, collaborative book page, etc. The ability to comment is an important part of involving members in a community dialogue.') .'</p>';
130
      $output .= '<p>'. t('An administrator can give comment permissions to user groups, and users can (optionally) edit their last comment, assuming no others have been posted since. Attached to each comment board is a control panel for customizing the way that comments are displayed. Users can control the chronological ordering of posts (newest or oldest first) and the number of posts to display on each page. Comments behave like other user submissions. Filters, smileys and HTML that work in nodes will also work with comments. The comment module provides specific features to inform site members when new comments have been posted.') .'</p>';
131
      $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="@comment">Comment page</a>.', array('@comment' => 'http://drupal.org/handbook/modules/comment/')) .'</p>';
132
      return $output;
133
134
    case 'admin/content/comment':
    case 'admin/content/comment/new':
135
      return '<p>'. t("Below is a list of the latest comments posted to your site. Click on a subject to see the comment, the author's name to edit the author's user information , 'edit' to modify the text, and 'delete' to remove their submission.") .'</p>';
136
    case 'admin/content/comment/approval':
137
      return '<p>'. t("Below is a list of the comments posted to your site that need approval. To approve a comment, click on 'edit' and then change its 'moderation status' to Approved. Click on a subject to see the comment, the author's name to edit the author's user information, 'edit' to modify the text, and 'delete' to remove their submission.") .'</p>';
138
    case 'admin/content/comment/settings':
139
      return '<p>'. t("Comments can be attached to any node, and their settings are below. The display comes in two types: a 'flat list' where everything is flush to the left side, and comments come in chronological order, and a 'threaded list' where replies to other comments are placed immediately below and slightly indented, forming an outline. They also come in two styles: 'expanded', where you see both the title and the contents, and 'collapsed' where you only see the title. Preview comment forces a user to look at their comment by clicking on a 'Preview' button before they can actually add the comment.") .'</p>';
140
   }
Dries's avatar
   
Dries committed
141
142
}

143
144
145
146
function _comment_view_access($node, $cid) {
  return $node && $cid;
}

Dries's avatar
   
Dries committed
147
148
149
/**
 * Implementation of hook_menu().
 */
150
151
152
153
154
155
156
function comment_menu() {
  $items['admin/content/comment'] = array(
    'title' => t('Comments'),
    'description' => t('List and edit site comments and the comment moderation queue.'),
    'page callback' => 'comment_admin',
    'access arguments' => array('administer comments'),
  );
Dries's avatar
   
Dries committed
157

158
159
160
161
162
163
  // Tabs:
  $items['admin/content/comment/list'] = array(
    'title' => t('List'),
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
Dries's avatar
   
Dries committed
164

165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
  // Subtabs:
  $items['admin/content/comment/list/new'] = array(
    'title' => t('Published comments'),
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['admin/content/comment/list/approval'] = array(
    'title' => t('Approval queue'),
    'page arguments' => array('approval'),
    'type' => MENU_LOCAL_TASK,
  );

  $items['admin/content/comment/settings'] = array(
    'title' => t('Settings'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('comment_admin_settings'),
    'weight' => 10,
    'type' => MENU_LOCAL_TASK,
  );

  $items['comment/delete'] = array(
    'title' => t('Delete comment'),
    'page callback' => 'comment_delete',
    'access arguments' => array('administer comments'),
    'type' => MENU_CALLBACK,
  );

  $items['comment/edit'] = array(
    'title' => t('Edit comment'),
    'page callback' => 'comment_edit',
    'access arguments' => array('post comments'),
    'type' => MENU_CALLBACK,
  );
198
  $items['comment/reply/%node'] = array(
199
200
    'title' => t('Reply to comment'),
    'page callback' => 'comment_reply',
201
    'page arguments' => array(2),
202
203
204
205
    'access callback' => 'node_access',
    'access arguments' => array('view', 2),
    'type' => MENU_CALLBACK,
  );
206
  $items['node/%node/%'] = array(
207
208
209
210
211
212
213
    'title' => t('View'),
    'page callback' => 'node_page_view',
    'page arguments' => array(1, 2),
    'access callback' => '_comment_view_access',
    'access arguments' => array(1, 2),
    'type' => MENU_CALLBACK,
  );
Dries's avatar
   
Dries committed
214
215
216
217
218
219
220
221

  return $items;
}

/**
 * Implementation of hook_perm().
 */
function comment_perm() {
222
  return array('access comments', 'post comments', 'administer comments', 'post comments without approval');
Dries's avatar
   
Dries committed
223
224
225
226
227
228
229
230
231
232
233
234
}

/**
 * Implementation of hook_block().
 *
 * Generates a block with the most recent comments.
 */
function comment_block($op = 'list', $delta = 0) {
  if ($op == 'list') {
    $blocks[0]['info'] = t('Recent comments');
    return $blocks;
  }
235
  else if ($op == 'view' && user_access('access comments')) {
Dries's avatar
   
Dries committed
236
    $block['subject'] = t('Recent comments');
237
    $block['content'] = theme('comment_block');
Dries's avatar
   
Dries committed
238
239
240
241
    return $block;
  }
}

242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/**
 * Find a number of recent comments. This is done in two steps.
 *   1. Find the n (specified by $number) nodes that have the most recent
 *      comments.  This is done by querying node_comment_statistics which has
 *      an index on last_comment_timestamp, and is thus a fast query.
 *   2. Loading the information from the comments table based on the nids found
 *      in step 1.
 *
 * @param $number (optional) The maximum number of comments to find.
 * @return $comments An array of comment objects each containing a nid,
 *   subject, cid, and timstamp, or an empty array if there are no recent
 *   comments visible to the current user.
 */
function comment_get_recent($number = 10) {
  // Select the $number nodes (visible to the current user) with the most
  // recent comments. This is efficient due to the index on
  // last_comment_timestamp.
259
  $result = db_query_range(db_rewrite_sql("SELECT nc.nid FROM {node_comment_statistics} nc WHERE nc.comment_count > 0 ORDER BY nc.last_comment_timestamp DESC", 'nc'), 0, $number);
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284

  $nids = array();
  while ($row = db_fetch_object($result)) {
    $nids[] = $row->nid;
  }

  $comments = array();
  if (!empty($nids)) {
    // From among the comments on the nodes selected in the first query,
    // find the $number most recent comments.
    $result = db_query_range('SELECT c.nid, c.subject, c.cid, c.timestamp FROM {comments} c INNER JOIN {node} n ON n.nid = c.nid WHERE c.nid IN ('. implode(',', $nids) .') AND n.status = 1 AND c.status = %d ORDER BY c.timestamp DESC', COMMENT_PUBLISHED, 0, $number);
    while ($comment = db_fetch_object($result)) {
      $comments[] = $comment;
    }
  }

  return $comments;
}

/**
 * Returns a formatted list of recent comments to be displayed in the comment
 * block.
 *
 * @ingroup themeable
 */
285
286
function theme_comment_block() {
  $items = array();
287
  foreach (comment_get_recent() as $comment) {
288
    $items[] = l($comment->subject, 'node/'. $comment->nid, array('fragment' => 'comment-'. $comment->cid)) .'<br />'. t('@time ago', array('@time' => format_interval(time() - $comment->timestamp)));
289
  }
290
291
292
  if ($items) {
    return theme('item_list', $items);
  }
293
294
}

Dries's avatar
   
Dries committed
295
296
297
/**
 * Implementation of hook_link().
 */
298
function comment_link($type, $node = NULL, $teaser = FALSE) {
Dries's avatar
   
Dries committed
299
300
301
302
  $links = array();

  if ($type == 'node' && $node->comment) {

303
    if ($teaser) {
Dries's avatar
   
Dries committed
304
305
306
307
308
309
      // Main page: display the number of comments that have been posted.

      if (user_access('access comments')) {
        $all = comment_num_all($node->nid);

        if ($all) {
310
          $links['comment_comments'] = array(
311
            'title' => format_plural($all, '1 comment', '@count comments'),
312
313
            'href' => "node/$node->nid",
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
314
            'fragment' => 'comments'
315
          );
316
317

          $new = comment_num_new($node->nid);
Dries's avatar
   
Dries committed
318
319

          if ($new) {
320
            $links['comment_new_comments'] = array(
321
              'title' => format_plural($new, '1 new comment', '@count new comments'),
322
323
324
              'href' => "node/$node->nid",
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
              'fragment' => 'new'
325
            );
Dries's avatar
   
Dries committed
326
327
328
          }
        }
        else {
Dries's avatar
Dries committed
329
          if ($node->comment == COMMENT_NODE_READ_WRITE) {
Dries's avatar
   
Dries committed
330
            if (user_access('post comments')) {
331
              $links['comment_add'] = array(
332
                'title' => t('Add new comment'),
333
334
                'href' => "comment/reply/$node->nid",
                'attributes' => array('title' => t('Add a new comment to this page.')),
335
                'fragment' => 'comment-form'
336
              );
Dries's avatar
   
Dries committed
337
338
            }
            else {
339
              $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node->nid);
Dries's avatar
   
Dries committed
340
341
342
343
344
345
346
347
348
            }
          }
        }
      }
    }
    else {
      // Node page: add a "post comment" link if the user is allowed to
      // post comments, if this node is not read-only, and if the comment form isn't already shown

349
      if ($node->comment == COMMENT_NODE_READ_WRITE) {
Dries's avatar
   
Dries committed
350
        if (user_access('post comments')) {
351
          if (variable_get('comment_form_location', COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_SEPARATE_PAGE) {
352
            $links['comment_add'] = array(
353
              'title' => t('Add new comment'),
354
355
              'href' => "comment/reply/$node->nid",
              'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
356
              'fragment' => 'comment-form'
357
            );
358
          }
Dries's avatar
   
Dries committed
359
360
        }
        else {
361
          $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node->nid);
Dries's avatar
   
Dries committed
362
363
364
365
366
367
        }
      }
    }
  }

  if ($type == 'comment') {
368
    $links = comment_links($node, $teaser);
Dries's avatar
   
Dries committed
369
  }
370
371
372
  if (isset($links['comment_forbidden'])) {
    $links['comment_forbidden']['html'] = TRUE;
  }
Dries's avatar
   
Dries committed
373
374
375
376

  return $links;
}

377
function comment_form_alter($form_id, &$form) {
378
379
380
381
  if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
    $form['workflow']['comment'] = array(
      '#type' => 'radios',
      '#title' => t('Default comment setting'),
382
      '#default_value' => variable_get('comment_'. $form['#node_type']->type, COMMENT_NODE_READ_WRITE),
383
384
385
386
387
      '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')),
      '#description' => t('Users with the <em>administer comments</em> permission will be able to override this setting.'),
    );
  }
  elseif (isset($form['type'])) {
388
    if ($form['type']['#value'] .'_node_form' == $form_id) {
389
      $node = $form['#node'];
390
391
392
393
394
395
396
397
398
399
400
401
402
403
      $form['comment_settings'] = array(
        '#type' => 'fieldset',
        '#access' => user_access('administer comments'),
        '#title' => t('Comment settings'),
        '#collapsible' => TRUE,
        '#collapsed' => TRUE,
        '#weight' => 30,
      );
      $form['comment_settings']['comment'] = array(
        '#type' => 'radios',
        '#parents' => array('comment'),
        '#default_value' => $node->comment,
        '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')),
      );
404
    }
405
406
407
  }
}

Dries's avatar
   
Dries committed
408
409
/**
 * Implementation of hook_nodeapi().
Dries's avatar
   
Dries committed
410
 *
Dries's avatar
   
Dries committed
411
412
413
 */
function comment_nodeapi(&$node, $op, $arg = 0) {
  switch ($op) {
Dries's avatar
   
Dries committed
414
    case 'load':
415
      return db_fetch_array(db_query("SELECT last_comment_timestamp, last_comment_name, comment_count FROM {node_comment_statistics} WHERE nid = %d", $node->nid));
416
417
418
419
      break;

    case 'prepare':
      if (!isset($node->comment)) {
Dries's avatar
Dries committed
420
        $node->comment = variable_get("comment_$node->type", COMMENT_NODE_READ_WRITE);
Dries's avatar
   
Dries committed
421
422
      }
      break;
423

424
425
426
427
428
429
430
431
432
    case 'insert':
      db_query('INSERT INTO {node_comment_statistics} (nid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count) VALUES (%d, %d, NULL, %d, 0)', $node->nid, $node->created, $node->uid);
      break;

    case 'delete':
      db_query('DELETE FROM {comments} WHERE nid = %d', $node->nid);
      db_query('DELETE FROM {node_comment_statistics} WHERE nid = %d', $node->nid);
      break;

Dries's avatar
Dries committed
433
434
    case 'update index':
      $text = '';
435
      $comments = db_query('SELECT subject, comment, format FROM {comments} WHERE nid = %d AND status = %d', $node->nid, COMMENT_PUBLISHED);
Dries's avatar
Dries committed
436
      while ($comment = db_fetch_object($comments)) {
437
        $text .= '<h2>'. check_plain($comment->subject) .'</h2>'. check_markup($comment->comment, $comment->format, FALSE);
Dries's avatar
Dries committed
438
439
      }
      return $text;
440

Dries's avatar
Dries committed
441
442
    case 'search result':
      $comments = db_result(db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = %d', $node->nid));
443
      return format_plural($comments, '1 comment', '@count comments');
444

Steven Wittens's avatar
- Typo    
Steven Wittens committed
445
    case 'rss item':
446
      if ($node->comment != COMMENT_NODE_DISABLED) {
447
        return array(array('key' => 'comments', 'value' => url('node/'. $node->nid, array('fragment' => 'comments', 'absolute' => TRUE))));
448
449
450
451
      }
      else {
        return array();
      }
Dries's avatar
   
Dries committed
452
453
454
455
456
457
458
459
460
461
462
  }
}

/**
 * Implementation of hook_user().
 *
 * Provides signature customization for the user's comments.
 */
function comment_user($type, $edit, &$user, $category = NULL) {
  if ($type == 'form' && $category == 'account') {
    // when user tries to edit his own data
463
464
465
466
467
468
469
470
471
472
    $form['comment_settings'] = array(
      '#type' => 'fieldset',
      '#title' => t('Comment settings'),
      '#collapsible' => TRUE,
      '#weight' => 4);
    $form['comment_settings']['signature'] = array(
      '#type' => 'textarea',
      '#title' => t('Signature'),
      '#default_value' => $edit['signature'],
      '#description' => t('Your signature will be publicly displayed at the end of your comments.'));
473
474

    return $form;
Dries's avatar
   
Dries committed
475
  }
476
477
478
479
  elseif ($type == 'delete') {
    db_query('UPDATE {comments} SET uid = 0 WHERE uid = %d', $user->uid);
    db_query('UPDATE {node_comment_statistics} SET last_comment_uid = 0 WHERE last_comment_uid = %d', $user->uid);
  }
Dries's avatar
   
Dries committed
480
481
}

482
/**
Dries's avatar
   
Dries committed
483
 * Menu callback; presents the comment settings page.
484
 */
Dries's avatar
Dries committed
485
function comment_admin_settings() {
486
487
  $form['viewing_options'] = array(
    '#type' => 'fieldset',
488
    '#title' => t('Viewing options'),
489
490
    '#collapsible' => TRUE,
  );
Dries's avatar
   
Dries committed
491

Dries's avatar
Dries committed
492
493
494
495
496
  $form['viewing_options']['comment_default_mode'] = array(
    '#type' => 'radios',
    '#title' => t('Default display mode'),
    '#default_value' => variable_get('comment_default_mode', COMMENT_MODE_THREADED_EXPANDED),
    '#options' => _comment_get_modes(),
497
    '#description' => t('The default view for comments. Expanded views display the body of the comment. Threaded views keep replies together.'),
Dries's avatar
Dries committed
498
  );
499

Dries's avatar
Dries committed
500
501
502
  $form['viewing_options']['comment_default_order'] = array(
    '#type' => 'radios',
    '#title' => t('Default display order'),
503
    '#default_value' => variable_get('comment_default_order', COMMENT_ORDER_NEWEST_FIRST),
Dries's avatar
Dries committed
504
    '#options' => _comment_get_orders(),
505
    '#description' => t('The default sorting for new users and anonymous users while viewing comments. These users may change their view using the comment control panel. For registered users, this change is remembered as a persistent user preference.'),
Dries's avatar
Dries committed
506
  );
Dries's avatar
   
Dries committed
507

508
  $form['viewing_options']['comment_default_per_page'] = array(
509
510
511
512
    '#type' => 'select',
    '#title' => t('Default comments per page'),
    '#default_value' => variable_get('comment_default_per_page', 50),
    '#options' => _comment_per_page(),
513
    '#description' => t('Default number of comments for each page: more comments are distributed in several pages.'),
514
515
  );

Dries's avatar
Dries committed
516
517
518
519
520
521
522
523
524
  $form['viewing_options']['comment_controls'] = array(
    '#type' => 'radios',
    '#title' => t('Comment controls'),
    '#default_value' => variable_get('comment_controls', COMMENT_CONTROLS_HIDDEN),
    '#options' => array(
      t('Display above the comments'),
      t('Display below the comments'),
      t('Display above and below the comments'),
      t('Do not display')),
525
    '#description' => t('Position of the comment controls box. The comment controls let the user change the default display mode and display order of comments.'),
Dries's avatar
Dries committed
526
  );
527

528
529
  $form['posting_settings'] = array(
    '#type' => 'fieldset',
530
    '#title' => t('Posting settings'),
531
532
    '#collapsible' => TRUE,
  );
533

Dries's avatar
Dries committed
534
535
  $form['posting_settings']['comment_anonymous'] = array(
    '#type' => 'radios',
536
    '#title' => t('Anonymous commenting'),
Dries's avatar
Dries committed
537
538
539
540
541
    '#default_value' => variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT),
    '#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'),
      COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information')),
542
    '#description' => t('This option is enabled when anonymous users have permission to post comments on the <a href="@url">permissions page</a>.', array('@url' => url('admin/user/access', array('fragment' => 'module-comment')))),
Dries's avatar
Dries committed
543
  );
544
  if (!user_access('post comments', user_load(array('uid' => 0)))) {
545
    $form['posting_settings']['comment_anonymous']['#disabled'] = TRUE;
546
  }
547
548

  $form['posting_settings']['comment_subject_field'] = array(
549
550
551
552
    '#type' => 'radios',
    '#title' => t('Comment subject field'),
    '#default_value' => variable_get('comment_subject_field', 1),
    '#options' => array(t('Disabled'), t('Enabled')),
553
    '#description' => t('Can users provide a unique subject for their comments?'),
554
555
  );

556
557
558
559
560
561
  $form['posting_settings']['comment_preview'] = array(
    '#type' => 'radios',
    '#title' => t('Preview comment'),
    '#default_value' => variable_get('comment_preview', COMMENT_PREVIEW_REQUIRED),
    '#options' => array(t('Optional'), t('Required')),
  );
562

Dries's avatar
Dries committed
563
564
565
566
  $form['posting_settings']['comment_form_location'] = array(
    '#type' => 'radios',
    '#title' => t('Location of comment submission form'),
    '#default_value' => variable_get('comment_form_location', COMMENT_FORM_SEPARATE_PAGE),
567
    '#options' => array(t('Display on separate page'), t('Display below post or comments')),
Dries's avatar
Dries committed
568
  );
569

570
  return system_settings_form($form);
Dries's avatar
   
Dries committed
571
572
}

573
574
575
576
577
578
/**
 * This is *not* a hook_access() implementation. This function is called
 * to determine whether the current user has access to a particular comment.
 *
 * Authenticated users can edit their comments as long they have not been
 * replied to. This prevents people from changing or revising their
579
 * statements based on the replies to their posts.
580
 */
Dries's avatar
   
Dries committed
581
function comment_access($op, $comment) {
Dries's avatar
   
Dries committed
582
583
  global $user;

584
  if ($op == 'edit') {
585
    return ($user->uid && $user->uid == $comment->uid && comment_num_replies($comment->cid) == 0) || user_access('administer comments');
Dries's avatar
   
Dries committed
586
587
  }
}
588

Dries's avatar
   
Dries committed
589
function comment_node_url() {
Dries's avatar
Dries committed
590
  return arg(0) .'/'. arg(1);
Dries's avatar
   
Dries committed
591
}
Dries's avatar
   
Dries committed
592

Dries's avatar
   
Dries committed
593
594
595
function comment_edit($cid) {
  global $user;

596
  $comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d', $cid));
Dries's avatar
   
Dries committed
597
  $comment = drupal_unpack($comment);
598
  $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
599
  if (comment_access('edit', $comment)) {
600
    return comment_form_box((array)$comment);
601
602
603
  }
  else {
    drupal_access_denied();
Dries's avatar
   
Dries committed
604
605
606
  }
}

607
608
609
610
611
612
613
614
615
616
617
/**
 * This function is responsible for generating a comment reply form.
 * There are several cases that have to be handled, including:
 *   - replies to comments
 *   - replies to nodes
 *   - attempts to reply to nodes that can no longer accept comments
 *   - respecting access permissions ('access comments', 'post comments', etc.)
 *
 * The node or comment that is being replied to must appear above the comment
 * form to provide the user context while authoring the comment.
 *
618
619
 * @param $node
 *   Every comment belongs to a node. This is that node.
620
621
622
623
624
625
626
 * @param $pid
 *   Some comments are replies to other comments. In those cases, $pid is the parent
 *   comment's cid.
 *
 * @return $output
 *   The rendered parent node or comment plus the new comment form.
 */
627
function comment_reply($node, $pid = NULL) {
628
  // Set the breadcrumb trail.
629
  menu_set_location(array(array('path' => "node/$node->nid", 'title' => $node->title), array('path' => "comment/reply/$node->nid")));
Dries's avatar
   
Dries committed
630

631
  $op = isset($_POST['op']) ? $_POST['op'] : '';
Dries's avatar
   
Dries committed
632

633
  $output = '';
Dries's avatar
   
Dries committed
634

Dries's avatar
Dries committed
635
  if (user_access('access comments')) {
636
    // The user is previewing a comment prior to submitting it.
637
638
    if ($op == t('Preview comment')) {
      if (user_access('post comments')) {
639
        $output .= comment_form_box(array('pid' => $pid, 'nid' => $node->nid), NULL);
640
641
642
      }
      else {
        drupal_set_message(t('You are not authorized to post comments.'), 'error');
643
        drupal_goto("node/$node->nid");
644
      }
Dries's avatar
   
Dries committed
645
646
    }
    else {
647
      // $pid indicates that this is a reply to a comment.
648
      if ($pid) {
649
        // load the comment whose cid = $pid
650
        if ($comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.picture, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d AND c.status = %d', $pid, COMMENT_PUBLISHED))) {
651
652
          // If that comment exists, make sure that the current comment and the parent comment both
          // belong to the same parent node.
653
          if ($comment->nid != $node->nid) {
654
655
            // Attempting to reply to a comment not belonging to the current nid.
            drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
656
            drupal_goto("node/$node->nid");
657
          }
658
          // Display the parent comment
659
660
661
662
663
664
          $comment = drupal_unpack($comment);
          $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
          $output .= theme('comment_view', $comment);
        }
        else {
          drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
665
          drupal_goto("node/$node->nid");
666
        }
667
      }
668
      // This is the case where the comment is in response to a node. Display the node.
669
670
671
672
      else if (user_access('access content')) {
        $output .= node_view($node);
      }

673
      // Should we show the reply box?
674
      if (node_comment_mode($node->nid) != COMMENT_NODE_READ_WRITE) {
675
        drupal_set_message(t("This discussion is closed: you can't post new comments."), 'error');
676
        drupal_goto("node/$node->nid");
677
678
      }
      else if (user_access('post comments')) {
679
        $output .= comment_form_box(array('pid' => $pid, 'nid' => $node->nid), t('Reply'));
680
681
682
      }
      else {
        drupal_set_message(t('You are not authorized to post comments.'), 'error');
683
        drupal_goto("node/$node->nid");
684
      }
Dries's avatar
   
Dries committed
685
    }
Kjartan's avatar
Kjartan committed
686
687
  }
  else {
688
    drupal_set_message(t('You are not authorized to view comments.'), 'error');
689
    drupal_goto("node/$node->nid");
Dries's avatar
   
Dries committed
690
  }
Dries's avatar
   
Dries committed
691

Dries's avatar
   
Dries committed
692
  return $output;
Dries's avatar
   
Dries committed
693
694
}

695
696
697
698
699
700
701
/**
 * Accepts a submission of new or changed comment content.
 *
 * @param $edit
 *   A comment array.
 *
 * @return
702
 *   If the comment is successfully saved the comment ID is returned. If the comment
703
704
705
 *   is not saved, FALSE is returned.
 */
function comment_save($edit) {
Dries's avatar
   
Dries committed
706
  global $user;
707
  if (user_access('post comments') && (user_access('administer comments') || node_comment_mode($edit['nid']) == COMMENT_NODE_READ_WRITE)) {
Dries's avatar
   
Dries committed
708
    if (!form_get_errors()) {
709
      if ($edit['cid']) {
710
        // Update the comment in the database.
711
        db_query("UPDATE {comments} SET status = %d, timestamp = %d, subject = '%s', comment = '%s', format = %d, uid = %d, name = '%s', mail = '%s', homepage = '%s' WHERE cid = %d", $edit['status'], $edit['timestamp'], $edit['subject'], $edit['comment'], $edit['format'], $edit['uid'], $edit['name'], $edit['mail'], $edit['homepage'], $edit['cid']);
Dries's avatar
   
Dries committed
712

Dries's avatar
   
Dries committed
713
714
        _comment_update_node_statistics($edit['nid']);

715
        // Allow modules to respond to the updating of a comment.
716
717
        comment_invoke_comment($edit, 'update');

Dries's avatar
Dries committed
718
        // Add an entry to the watchdog log.
719
        watchdog('content', t('Comment: updated %subject.', array('%subject' => $edit['subject'])), WATCHDOG_NOTICE, l(t('view'), 'node/'. $edit['nid'], array('fragment' => 'comment-'. $edit['cid'])));
Dries's avatar
   
Dries committed
720
721
      }
      else {
722
723
724
725
726
727
728
        // Check for duplicate comments. Note that we have to use the
        // validated/filtered data to perform such check.
        $duplicate = db_result(db_query("SELECT COUNT(cid) FROM {comments} WHERE pid = %d AND nid = %d AND subject = '%s' AND comment = '%s'", $edit['pid'], $edit['nid'], $edit['subject'], $edit['comment']), 0);
        if ($duplicate != 0) {
          watchdog('content', t('Comment: duplicate %subject.', array('%subject' => $edit['subject'])), WATCHDOG_WARNING);
        }

729
        // Add the comment to database.
730
        $status = user_access('post comments without approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
731
        $roles = variable_get('comment_roles', array());
Dries's avatar
   
Dries committed
732
733
734
735
736
737
        $score = 0;

        foreach (array_intersect(array_keys($roles), array_keys($user->roles)) as $rid) {
          $score = max($roles[$rid], $score);
        }

Dries's avatar
   
Dries committed
738
739
        $users = serialize(array(0 => $score));

740
741
        // Here we are building the thread field. See the documentation for
        // comment_render().
742
        if ($edit['pid'] == 0) {
743
744
          // This is a comment with no parent comment (depth 0): we start
          // by retrieving the maximum thread level.
745
          $max = db_result(db_query('SELECT MAX(thread) FROM {comments} WHERE nid = %d', $edit['nid']));
Dries's avatar
   
Dries committed
746

747
748
          // Strip the "/" from the end of the thread.
          $max = rtrim($max, '/');
Dries's avatar
   
Dries committed
749

750
          // Finally, build the thread field for this new comment.
751
          $thread = int2vancode(vancode2int($max) + 1) .'/';
Dries's avatar
   
Dries committed
752
753
        }
        else {
754
755
          // This is comment with a parent comment: we increase
          // the part of the thread value at the proper depth.
Dries's avatar
   
Dries committed
756
757

          // Get the parent comment:
758
          $parent = _comment_load($edit['pid']);
Dries's avatar
   
Dries committed
759

760
          // Strip the "/" from the end of the parent thread.
761
          $parent->thread = (string) rtrim((string) $parent->thread, '/');
Dries's avatar
   
Dries committed
762

763
          // Get the max value in _this_ thread.
Dries's avatar
   
Dries committed
764
          $max = db_result(db_query("SELECT MAX(thread) FROM {comments} WHERE thread LIKE '%s.%%' AND nid = %d", $parent->thread, $edit['nid']));
Dries's avatar
   
Dries committed
765

766
767
          if ($max == '') {
            // First child of this parent.
768
            $thread = $parent->thread .'.'. int2vancode(0) .'/';
Dries's avatar
   
Dries committed
769
770
          }
          else {
771
772
            // Strip the "/" at the end of the thread.
            $max = rtrim($max, '/');
Dries's avatar
   
Dries committed
773

774
775
776
            // We need to get the value at the correct depth.
            $parts = explode('.', $max);
            $parent_depth = count(explode('.', $parent->thread));
Dries's avatar
   
Dries committed
777
778
            $last = $parts[$parent_depth];

779
            // Finally, build the thread field for this new comment.
780
            $thread = $parent->thread .'.'. int2vancode(vancode2int($last) + 1) .'/';
Dries's avatar
   
Dries committed
781
782
783
          }
        }

784
        $edit['cid'] = db_next_id('{comments}_cid');
Dries's avatar
   
Dries committed
785
786
        $edit['timestamp'] = time();

787
        if ($edit['uid'] === $user->uid) { // '===' because we want to modify anonymous users too
Dries's avatar
   
Dries committed
788
789
790
          $edit['name'] = $user->name;
        }

791
        $edit += array('mail' => '', 'homepage' => '');
792
        db_query("INSERT INTO {comments} (cid, nid, pid, uid, subject, comment, format, hostname, timestamp, status, score, users, thread, name, mail, homepage) VALUES (%d, %d, %d, %d, '%s', '%s', %d, '%s', %d, %d, %d, '%s', '%s', '%s', '%s', '%s')", $edit['cid'], $edit['nid'], $edit['pid'], $edit['uid'], $edit['subject'], $edit['comment'], $edit['format'], $_SERVER['REMOTE_ADDR'], $edit['timestamp'], $status, $score, $users, $thread, $edit['name'], $edit['mail'], $edit['homepage']);
Dries's avatar
   
Dries committed
793
794

        _comment_update_node_statistics($edit['nid']);
Dries's avatar
   
Dries committed
795

796
        // Tell the other modules a new comment has been submitted.
797
        comment_invoke_comment($edit, 'insert');
Dries's avatar
   
Dries committed
798

799
        // Add an entry to the watchdog log.
800
        watchdog('content', t('Comment: added %subject.', array('%subject' => $edit['subject'])), WATCHDOG_NOTICE, l(t('view'), 'node/'. $edit['nid'], array('fragment' => 'comment-'. $edit['cid'])));
Dries's avatar
   
Dries committed
801
      }
Dries's avatar
   
Dries committed
802

803
      // Clear the cache so an anonymous user can see his comment being added.
Dries's avatar
   
Dries committed
804
      cache_clear_all();
Dries's avatar
   
Dries committed
805

Dries's avatar
   
Dries committed
806
      // Explain the approval queue if necessary, and then
Dries's avatar
   
Dries committed
807
      // redirect the user to the node he's commenting on.
808
      if ($status == COMMENT_NOT_PUBLISHED) {
Dries's avatar
   
Dries committed
809
        drupal_set_message(t('Your comment has been queued for moderation by site administrators and will be published after approval.'));
Dries's avatar
   
Dries committed
810
      }
811
      return $edit['cid'];
Dries's avatar
   
Dries committed
812
813
    }
    else {
814
      return FALSE;
Dries's avatar
   
Dries committed
815
816
    }
  }
Dries's avatar
   
Dries committed
817
  else {
818
    $txt = t('Comment: unauthorized comment submitted or comment submitted to a closed node %subject.', array('%subject' => $edit['subject']));
819
820
821
    watchdog('content', $txt, WATCHDOG_WARNING);
    drupal_set_message($txt, 'error');
    return FALSE;
Dries's avatar
   
Dries committed
822
823
824
825
  }
}

function comment_links($comment, $return = 1) {
Dries's avatar
   
Dries committed
826
  global $user;
Dries's avatar
   
Dries committed
827

Dries's avatar
   
Dries committed
828
  $links = array();
Dries's avatar
   
Dries committed
829

830
  // If we are viewing just this comment, we link back to the node.
Dries's avatar
   
Dries committed
831
  if ($return) {
832
    $links['comment_parent'] = array(
833
834
835
      'title' => t('parent'),
      'href' => comment_node_url(),
      'fragment' => "comment-$comment->cid"
836
    );
Dries's avatar
   
Dries committed
837
  }
Dries's avatar
   
Dries committed
838

839
  if (node_comment_mode($comment->nid) == COMMENT_NODE_READ_WRITE) {
840
    if (user_access('administer comments') && user_access('post comments')) {
841
      $links['comment_delete'] = array(
842
843
        'title' => t('delete'),
        'href' => "comment/delete/$comment->cid"
844
845
      );
      $links['comment_edit'] = array(
846
847
        'title' => t('edit'),
        'href' => "comment/edit/$comment->cid"
848
849
      );
      $links['comment_reply'] = array(
850
851
        'title' => t('reply'),
        'href' => "comment/reply/$comment->nid/$comment->cid"
852
      );
853
    }
854
855
    else if (user_access('post comments')) {
      if (comment_access('edit', $comment)) {
856
        $links['comment_edit'] = array(
857
858
          'title' => t('edit'),
          'href' => "comment/edit/$comment->cid"
859
        );
Dries's avatar
   
Dries committed
860
      }
861
      $links['comment_reply'] = array(
862
863
        'title' => t('reply'),
        'href' => "comment/reply/$comment->nid/$comment->cid"
864
      );
Dries's avatar
   
Dries committed
865
866
    }
    else {
867
      $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $comment->nid);
Dries's avatar
   
Dries committed
868
    }
Dries's avatar
   
Dries committed
869
  }
Dries's avatar
   
Dries committed
870

Dries's avatar
   
Dries committed
871
  return $links;
Dries's avatar
   
Dries committed
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
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
935
/**
 * Renders comment(s).
 *
 * @param $node
 *   The node which comment(s) needs rendering.
 * @param $cid
 *   Optional, if given, only one comment is rendered.
 *
 * 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 out
 * thread fields will look like depicted below:
 *
 * 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.
 */
Dries's avatar
   
Dries committed
936
function comment_render($node, $cid = 0) {
Dries's avatar
   
Dries committed
937
938
  global $user;

939
  $output = '';
Dries's avatar
   
Dries committed
940

941
942
  if (user_access('access comments')) {
    // Pre-process variables.
Dries's avatar
   
Dries committed
943
    $nid = $node->nid;
Dries's avatar
   
Dries committed
944
945
    if (empty($nid)) {
      $nid = 0;
Dries's avatar
   
Dries committed
946
947
    }

948
949
950
    $mode = _comment_get_display_setting('mode');
    $order = _comment_get_display_setting('sort');
    $comments_per_page = _comment_get_display_setting('comments_per_page');
Dries's avatar
   
Dries committed
951

Kjartan's avatar
Kjartan committed
952
    if ($cid) {
953
      // Single comment view.
954
955
956
957
958
959
      $query = 'SELECT c.cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.picture, u.data, c.score, c.users, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d';
      $query_args = array($cid);
      if (!user_access('administer comments')) {
        $query .= ' AND c.status = %d';
        $query_args[] = COMMENT_PUBLISHED;
      }
960

961
      $result = db_query($query, $query_args);
Dries's avatar
   
Dries committed
962

Dries's avatar
   
Dries committed
963
      if ($comment = db_fetch_object($result)) {
964
        $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
965
966
967
968
969
970
971
972
        $links = module_invoke_all('link', 'comment', $comment, 1);

        foreach (module_implements('link_alter') as $module) {
          $function = $module .'_link_alter';
          $function($node, $links);
        }

        $output .= theme('comment_view',