comment.module 72.8 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
}

Dries's avatar
   
Dries committed
143
144
145
/**
 * Implementation of hook_menu().
 */
Dries's avatar
   
Dries committed
146
function comment_menu($may_cache) {
Dries's avatar
   
Dries committed
147
148
  $items = array();

Dries's avatar
   
Dries committed
149
150
  if ($may_cache) {
    $access = user_access('administer comments');
151
152
    $items[] = array(
      'path' => 'admin/content/comment',
153
      'title' => t('Comments'),
154
      'description' => t('List and edit site comments and the comment moderation queue.'),
155
      'callback' => 'comment_admin',
156
157
      'access' => $access
    );
Dries's avatar
   
Dries committed
158
159

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

    // Subtabs:
164
    $items[] = array('path' => 'admin/content/comment/list/new', 'title' => t('Published comments'),
Dries's avatar
   
Dries committed
165
      'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
166
    $items[] = array('path' => 'admin/content/comment/list/approval', 'title' => t('Approval queue'),
167
168
      'callback' => 'comment_admin',
      'callback arguments' => array('approval'),
169
      'access' => $access,
Dries's avatar
   
Dries committed
170
171
      'type' => MENU_LOCAL_TASK);

Dries's avatar
Dries committed
172
    $items[] = array(
173
      'path' => 'admin/content/comment/settings',
174
      'title' => t('Settings'),
175
176
      'callback' => 'drupal_get_form',
      'callback arguments' => array('comment_admin_settings'),
Dries's avatar
Dries committed
177
      'access' => $access,
178
179
      'weight' => 10,
      'type' => MENU_LOCAL_TASK);
180

181
    $items[] = array('path' => 'comment/delete', 'title' => t('Delete comment'),
182
      'callback' => 'comment_delete', 'access' => $access, 'type' => MENU_CALLBACK);
Dries's avatar
   
Dries committed
183
184

    $access = user_access('post comments');
185
    $items[] = array('path' => 'comment/edit', 'title' => t('Edit comment'),
186
187
      'callback' => 'comment_edit',
      'access' => $access, 'type' => MENU_CALLBACK);
Dries's avatar
   
Dries committed
188
  }
Dries's avatar
   
Dries committed
189
190
  else {
    if (arg(0) == 'comment' && arg(1) == 'reply' && is_numeric(arg(2))) {
191
      $node = node_load(arg(2));
Dries's avatar
   
Dries committed
192
      if ($node->nid) {
193
        $items[] = array('path' => 'comment/reply', 'title' => t('Reply to comment'),
Dries's avatar
   
Dries committed
194
195
196
197
          'callback' => 'comment_reply', 'access' => node_access('view', $node), 'type' => MENU_CALLBACK);
      }
    }
    if ((arg(0) == 'node') && is_numeric(arg(1)) && is_numeric(arg(2))) {
198
199
200
201
202
203
204
      $items[] = array(
        'path' => ('node/'. arg(1) .'/'. arg(2)),
        'title' => t('View'),
        'callback' => 'node_page_view',
        'callback arguments' => array(node_load(arg(1)), arg(2)),
        'type' => MENU_CALLBACK,
      );
Dries's avatar
   
Dries committed
205
    }
Dries's avatar
   
Dries committed
206
  }
Dries's avatar
   
Dries committed
207
208
209
210
211
212
213
214

  return $items;
}

/**
 * Implementation of hook_perm().
 */
function comment_perm() {
215
  return array('access comments', 'post comments', 'administer comments', 'post comments without approval');
Dries's avatar
   
Dries committed
216
217
218
219
220
221
222
223
224
225
226
227
}

/**
 * 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;
  }
228
  else if ($op == 'view' && user_access('access comments')) {
Dries's avatar
   
Dries committed
229
    $block['subject'] = t('Recent comments');
230
    $block['content'] = theme('comment_block');
Dries's avatar
   
Dries committed
231
232
233
234
    return $block;
  }
}

235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/**
 * 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.
  $result = db_query_range(db_rewrite_sql("SELECT n.nid FROM {node_comment_statistics} n WHERE n.comment_count > 0 ORDER BY n.last_comment_timestamp DESC"), 0, $number);

  $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
 */
278
279
function theme_comment_block() {
  $items = array();
280
  foreach (comment_get_recent() as $comment) {
281
    $items[] = l($comment->subject, 'node/'. $comment->nid, NULL, NULL, 'comment-'. $comment->cid) .'<br />'. t('@time ago', array('@time' => format_interval(time() - $comment->timestamp)));
282
  }
283
284
285
  if ($items) {
    return theme('item_list', $items);
  }
286
287
}

Dries's avatar
   
Dries committed
288
289
290
/**
 * Implementation of hook_link().
 */
291
function comment_link($type, $node = NULL, $teaser = FALSE) {
Dries's avatar
   
Dries committed
292
293
294
295
  $links = array();

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

296
    if ($teaser) {
Dries's avatar
   
Dries committed
297
298
299
300
301
302
      // Main page: display the number of comments that have been posted.

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

        if ($all) {
303
          $links['comment_comments'] = array(
304
            'title' => format_plural($all, '1 comment', '@count comments'),
305
306
            'href' => "node/$node->nid",
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
307
            'fragment' => 'comments'
308
          );
309
310

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

          if ($new) {
313
            $links['comment_new_comments'] = array(
314
              'title' => format_plural($new, '1 new comment', '@count new comments'),
315
316
317
              'href' => "node/$node->nid",
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
              'fragment' => 'new'
318
            );
Dries's avatar
   
Dries committed
319
320
321
          }
        }
        else {
Dries's avatar
Dries committed
322
          if ($node->comment == COMMENT_NODE_READ_WRITE) {
Dries's avatar
   
Dries committed
323
            if (user_access('post comments')) {
324
              $links['comment_add'] = array(
325
                'title' => t('Add new comment'),
326
327
                'href' => "comment/reply/$node->nid",
                'attributes' => array('title' => t('Add a new comment to this page.')),
328
                'fragment' => 'comment-form'
329
              );
Dries's avatar
   
Dries committed
330
331
            }
            else {
332
              $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node->nid);
Dries's avatar
   
Dries committed
333
334
335
336
337
338
339
340
341
            }
          }
        }
      }
    }
    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

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

  if ($type == 'comment') {
361
    $links = comment_links($node, $teaser);
Dries's avatar
   
Dries committed
362
  }
363
364
365
  if (isset($links['comment_forbidden'])) {
    $links['comment_forbidden']['html'] = TRUE;
  }
Dries's avatar
   
Dries committed
366
367
368
369

  return $links;
}

370
function comment_form_alter($form_id, &$form) {
371
372
373
374
  if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
    $form['workflow']['comment'] = array(
      '#type' => 'radios',
      '#title' => t('Default comment setting'),
375
      '#default_value' => variable_get('comment_'. $form['#node_type']->type, COMMENT_NODE_READ_WRITE),
376
377
378
379
380
      '#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'])) {
381
    if ($form['type']['#value'] .'_node_form' == $form_id) {
382
      $node = $form['#node'];
383
384
385
386
387
388
389
390
391
392
393
394
395
396
      $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')),
      );
397
    }
398
399
400
  }
}

Dries's avatar
   
Dries committed
401
402
/**
 * Implementation of hook_nodeapi().
Dries's avatar
   
Dries committed
403
 *
Dries's avatar
   
Dries committed
404
405
406
 */
function comment_nodeapi(&$node, $op, $arg = 0) {
  switch ($op) {
Dries's avatar
   
Dries committed
407
    case 'load':
408
      return db_fetch_array(db_query("SELECT last_comment_timestamp, last_comment_name, comment_count FROM {node_comment_statistics} WHERE nid = %d", $node->nid));
409
410
411
412
      break;

    case 'prepare':
      if (!isset($node->comment)) {
Dries's avatar
Dries committed
413
        $node->comment = variable_get("comment_$node->type", COMMENT_NODE_READ_WRITE);
Dries's avatar
   
Dries committed
414
415
      }
      break;
416

417
418
419
420
421
422
423
424
425
    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
426
427
    case 'update index':
      $text = '';
428
      $comments = db_query('SELECT subject, comment, format FROM {comments} WHERE nid = %d AND status = %d', $node->nid, COMMENT_PUBLISHED);
Dries's avatar
Dries committed
429
      while ($comment = db_fetch_object($comments)) {
430
        $text .= '<h2>'. check_plain($comment->subject) .'</h2>'. check_markup($comment->comment, $comment->format, FALSE);
Dries's avatar
Dries committed
431
432
      }
      return $text;
433

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

Steven Wittens's avatar
- Typo    
Steven Wittens committed
438
    case 'rss item':
439
      if ($node->comment != COMMENT_NODE_DISABLED) {
440
        return array(array('key' => 'comments', 'value' => url('node/'. $node->nid, NULL, 'comments', TRUE)));
441
442
443
444
      }
      else {
        return array();
      }
Dries's avatar
   
Dries committed
445
446
447
448
449
450
451
452
453
454
455
  }
}

/**
 * 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
456
457
458
459
460
461
462
463
464
465
    $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.'));
466
467

    return $form;
Dries's avatar
   
Dries committed
468
  }
469
470
471
472
  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
473
474
}

475
/**
Dries's avatar
   
Dries committed
476
 * Menu callback; presents the comment settings page.
477
 */
Dries's avatar
Dries committed
478
function comment_admin_settings() {
479
480
  $form['viewing_options'] = array(
    '#type' => 'fieldset',
481
    '#title' => t('Viewing options'),
482
483
    '#collapsible' => TRUE,
  );
Dries's avatar
   
Dries committed
484

Dries's avatar
Dries committed
485
486
487
488
489
  $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(),
490
    '#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
491
  );
492

Dries's avatar
Dries committed
493
494
495
  $form['viewing_options']['comment_default_order'] = array(
    '#type' => 'radios',
    '#title' => t('Default display order'),
496
    '#default_value' => variable_get('comment_default_order', COMMENT_ORDER_NEWEST_FIRST),
Dries's avatar
Dries committed
497
    '#options' => _comment_get_orders(),
498
    '#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
499
  );
Dries's avatar
   
Dries committed
500

501
  $form['viewing_options']['comment_default_per_page'] = array(
502
503
504
505
    '#type' => 'select',
    '#title' => t('Default comments per page'),
    '#default_value' => variable_get('comment_default_per_page', 50),
    '#options' => _comment_per_page(),
506
    '#description' => t('Default number of comments for each page: more comments are distributed in several pages.'),
507
508
  );

Dries's avatar
Dries committed
509
510
511
512
513
514
515
516
517
  $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')),
518
    '#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
519
  );
520

521
522
  $form['posting_settings'] = array(
    '#type' => 'fieldset',
523
    '#title' => t('Posting settings'),
524
525
    '#collapsible' => TRUE,
  );
526

Dries's avatar
Dries committed
527
528
  $form['posting_settings']['comment_anonymous'] = array(
    '#type' => 'radios',
529
    '#title' => t('Anonymous commenting'),
Dries's avatar
Dries committed
530
531
532
533
534
    '#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')),
535
    '#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', NULL, 'module-comment'))),
Dries's avatar
Dries committed
536
  );
537
  if (!user_access('post comments', user_load(array('uid' => 0)))) {
538
    $form['posting_settings']['comment_anonymous']['#disabled'] = TRUE;
539
  }
540
541

  $form['posting_settings']['comment_subject_field'] = array(
542
543
544
545
    '#type' => 'radios',
    '#title' => t('Comment subject field'),
    '#default_value' => variable_get('comment_subject_field', 1),
    '#options' => array(t('Disabled'), t('Enabled')),
546
    '#description' => t('Can users provide a unique subject for their comments?'),
547
548
  );

549
550
551
552
553
554
  $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')),
  );
555

Dries's avatar
Dries committed
556
557
558
559
  $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),
560
    '#options' => array(t('Display on separate page'), t('Display below post or comments')),
Dries's avatar
Dries committed
561
  );
562

563
  return system_settings_form($form);
Dries's avatar
   
Dries committed
564
565
}

566
567
568
569
570
571
/**
 * 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
572
 * statements based on the replies to their posts.
573
 */
Dries's avatar
   
Dries committed
574
function comment_access($op, $comment) {
Dries's avatar
   
Dries committed
575
576
  global $user;

577
  if ($op == 'edit') {
578
    return ($user->uid && $user->uid == $comment->uid && comment_num_replies($comment->cid) == 0) || user_access('administer comments');
Dries's avatar
   
Dries committed
579
580
  }
}
581

Dries's avatar
   
Dries committed
582
function comment_node_url() {
Dries's avatar
Dries committed
583
  return arg(0) .'/'. arg(1);
Dries's avatar
   
Dries committed
584
}
Dries's avatar
   
Dries committed
585

Dries's avatar
   
Dries committed
586
587
588
function comment_edit($cid) {
  global $user;

589
  $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
590
  $comment = drupal_unpack($comment);
591
  $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
592
  if (comment_access('edit', $comment)) {
593
    return comment_form_box((array)$comment);
594
595
596
  }
  else {
    drupal_access_denied();
Dries's avatar
   
Dries committed
597
598
599
  }
}

600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
/**
 * 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.
 *
 * @param $nid
 *   Every comment belongs to a node. This is that node's id.
 * @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.
 */
Dries's avatar
Dries committed
620
function comment_reply($nid, $pid = NULL) {
621
  // Load the parent node.
622
  $node = node_load($nid);
623
624

  // Set the breadcrumb trail.
625
  menu_set_location(array(array('path' => "node/$nid", 'title' => $node->title), array('path' => "comment/reply/$nid")));
Dries's avatar
   
Dries committed
626

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

629
  $output = '';
Dries's avatar
   
Dries committed
630

Dries's avatar
Dries committed
631
  if (user_access('access comments')) {
632
    // The user is previewing a comment prior to submitting it.
633
634
    if ($op == t('Preview comment')) {
      if (user_access('post comments')) {
635
        $output .= comment_form_box(array('pid' => $pid, 'nid' => $nid), NULL);
636
637
638
      }
      else {
        drupal_set_message(t('You are not authorized to post comments.'), 'error');
639
        drupal_goto("node/$nid");
640
      }
Dries's avatar
   
Dries committed
641
642
    }
    else {
643
      // $pid indicates that this is a reply to a comment.
644
      if ($pid) {
645
        // load the comment whose cid = $pid
646
        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))) {
647
648
          // If that comment exists, make sure that the current comment and the parent comment both
          // belong to the same parent node.
649
650
651
652
653
          if ($comment->nid != $nid) {
            // 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');
            drupal_goto("node/$nid");
          }
654
          // Display the parent comment
655
656
657
658
659
660
661
662
          $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');
          drupal_goto("node/$nid");
        }
663
      }
664
      // This is the case where the comment is in response to a node. Display the node.
665
666
667
668
      else if (user_access('access content')) {
        $output .= node_view($node);
      }

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

Dries's avatar
   
Dries committed
688
  return $output;
Dries's avatar
   
Dries committed
689
690
}

691
692
693
694
695
696
697
/**
 * Accepts a submission of new or changed comment content.
 *
 * @param $edit
 *   A comment array.
 *
 * @return
698
 *   If the comment is successfully saved the comment ID is returned. If the comment
699
700
701
 *   is not saved, FALSE is returned.
 */
function comment_save($edit) {
Dries's avatar
   
Dries committed
702
  global $user;
703
  if (user_access('post comments') && (user_access('administer comments') || node_comment_mode($edit['nid']) == COMMENT_NODE_READ_WRITE)) {
Dries's avatar
   
Dries committed
704
    if (!form_get_errors()) {
705
      if ($edit['cid']) {
706
        // Update the comment in the database.
707
        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
708

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

711
        // Allow modules to respond to the updating of a comment.
712
713
        comment_invoke_comment($edit, 'update');

Dries's avatar
Dries committed
714
        // Add an entry to the watchdog log.
715
        watchdog('content', t('Comment: updated %subject.', array('%subject' => $edit['subject'])), WATCHDOG_NOTICE, l(t('view'), 'node/'. $edit['nid'], NULL, NULL, 'comment-'. $edit['cid']));
Dries's avatar
   
Dries committed
716
717
      }
      else {
718
719
720
721
722
723
724
        // 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);
        }

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

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

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

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

743
744
          // Strip the "/" from the end of the thread.
          $max = rtrim($max, '/');
Dries's avatar
   
Dries committed
745

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

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

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

759
          // Get the max value in _this_ thread.
Dries's avatar
   
Dries committed
760
          $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
761

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

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

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

780
        $edit['cid'] = db_next_id('{comments}_cid');
Dries's avatar
   
Dries committed
781
782
        $edit['timestamp'] = time();

783
        if ($edit['uid'] === $user->uid) { // '===' because we want to modify anonymous users too
Dries's avatar
   
Dries committed
784
785
786
          $edit['name'] = $user->name;
        }

787
        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
788
789

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

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

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

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

Dries's avatar
   
Dries committed
801
      // Explain the approval queue if necessary, and then
Dries's avatar
   
Dries committed
802
      // redirect the user to the node he's commenting on.
803
      if ($status == COMMENT_NOT_PUBLISHED) {
Dries's avatar
   
Dries committed
804
        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
805
      }
806
      return $edit['cid'];
Dries's avatar
   
Dries committed
807
808
    }
    else {
809
      return FALSE;
Dries's avatar
   
Dries committed
810
811
    }
  }
Dries's avatar
   
Dries committed
812
  else {
813
    $txt = t('Comment: unauthorized comment submitted or comment submitted to a closed node %subject.', array('%subject' => $edit['subject']));
814
815
816
    watchdog('content', $txt, WATCHDOG_WARNING);
    drupal_set_message($txt, 'error');
    return FALSE;
Dries's avatar
   
Dries committed
817
818
819
820
  }
}

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

Dries's avatar
   
Dries committed
823
  $links = array();
Dries's avatar
   
Dries committed
824

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

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

Dries's avatar
   
Dries committed
866
  return $links;
Dries's avatar
   
Dries committed
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
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
/**
 * 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
931
function comment_render($node, $cid = 0) {
Dries's avatar
   
Dries committed
932
933
  global $user;

934
  $output = '';
Dries's avatar
   
Dries committed
935

936
937
  if (user_access('access comments')) {
    // Pre-process variables.
Dries's avatar
   
Dries committed
938
    $nid = $node->nid;
Dries's avatar
   
Dries committed
939
940
    if (empty($nid)) {
      $nid = 0;
Dries's avatar
   
Dries committed
941