comment.module 78.5 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
127
function comment_help($path, $arg) {
  switch ($path) {
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
    case 'admin/content/comment':
134
      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>';
135
    case 'admin/content/comment/list/approval':
136
      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>';
137
    case 'admin/content/comment/settings':
138
      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>';
139
  }
Dries's avatar
   
Dries committed
140
141
}

142
143
144
145
146
147
148
149
150
151
152
153
/**
 * Implementation of hook_theme()
 */
function comment_theme() {
  return array(
    'comment_block' => array(
      'arguments' => array(),
    ),
    'comment_admin_overview' => array(
      'arguments' => array('form' => NULL),
    ),
    'comment_preview' => array(
154
      'arguments' => array('comment' => NULL, 'node' => NULL, 'links' => array(), 'visible' => 1),
155
156
    ),
    'comment_view' => array(
157
      'arguments' => array('comment' => NULL, 'node' => NULL, 'links' => array(), 'visible' => 1),
158
159
160
161
162
    ),
    'comment_controls' => array(
      'arguments' => array('form' => NULL),
    ),
    'comment' => array(
163
      'template' => 'comment',
164
      'arguments' => array('comment' => NULL, 'node' => NULL, 'links' => array()),
165
166
    ),
    'comment_folded' => array(
167
      'template' => 'comment-folded',
168
169
170
      'arguments' => array('comment' => NULL),
    ),
    'comment_flat_collapsed' => array(
171
      'arguments' => array('comment' => NULL, 'node' => NULL),
172
173
    ),
    'comment_flat_expanded' => array(
174
      'arguments' => array('comment' => NULL, 'node' => NULL),
175
176
    ),
    'comment_thread_collapsed' => array(
177
      'arguments' => array('comment' => NULL, 'node' => NULL),
178
179
    ),
    'comment_thread_expanded' => array(
180
      'arguments' => array('comment' => NULL, 'node' => NULL),
181
182
183
184
185
    ),
    'comment_post_forbidden' => array(
      'arguments' => array('nid' => NULL),
    ),
    'comment_wrapper' => array(
186
      'template' => 'comment-wrapper',
187
      'arguments' => array('content' => NULL, 'node' => NULL),
188
    ),
189
190
191
    'comment_submitted' => array(
      'arguments' => array('comment' => NULL),
    ),
192
193
194
  );
}

Dries's avatar
   
Dries committed
195
196
197
/**
 * Implementation of hook_menu().
 */
198
199
function comment_menu() {
  $items['admin/content/comment'] = array(
200
201
    'title' => 'Comments',
    'description' => 'List and edit site comments and the comment moderation queue.',
202
203
204
    'page callback' => 'comment_admin',
    'access arguments' => array('administer comments'),
  );
Dries's avatar
   
Dries committed
205

206
207
  // Tabs:
  $items['admin/content/comment/list'] = array(
208
    'title' => 'List',
209
210
211
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
Dries's avatar
   
Dries committed
212

213
214
  // Subtabs:
  $items['admin/content/comment/list/new'] = array(
215
    'title' => 'Published comments',
216
217
218
219
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['admin/content/comment/list/approval'] = array(
220
    'title' => 'Approval queue',
221
222
223
224
225
    'page arguments' => array('approval'),
    'type' => MENU_LOCAL_TASK,
  );

  $items['admin/content/comment/settings'] = array(
226
    'title' => 'Settings',
227
228
229
230
231
232
233
    'page callback' => 'drupal_get_form',
    'page arguments' => array('comment_admin_settings'),
    'weight' => 10,
    'type' => MENU_LOCAL_TASK,
  );

  $items['comment/delete'] = array(
234
    'title' => 'Delete comment',
235
    'page callback' => 'comment_delete',
236
237
238
239
240
    'access arguments' => array('administer comments'),
    'type' => MENU_CALLBACK,
  );

  $items['comment/edit'] = array(
241
    'title' => 'Edit comment',
242
243
244
245
    'page callback' => 'comment_edit',
    'access arguments' => array('post comments'),
    'type' => MENU_CALLBACK,
  );
246
  $items['comment/reply/%node'] = array(
247
    'title' => 'Reply to comment',
248
    'page callback' => 'comment_reply',
249
    'page arguments' => array(2),
250
251
252
253
    'access callback' => 'node_access',
    'access arguments' => array('view', 2),
    'type' => MENU_CALLBACK,
  );
Dries's avatar
   
Dries committed
254
255
256
257
258
259
260
261

  return $items;
}

/**
 * Implementation of hook_perm().
 */
function comment_perm() {
262
  return array('access comments', 'post comments', 'administer comments', 'post comments without approval');
Dries's avatar
   
Dries committed
263
264
265
266
267
268
269
270
271
272
273
274
}

/**
 * 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;
  }
275
  else if ($op == 'view' && user_access('access comments')) {
Dries's avatar
   
Dries committed
276
    $block['subject'] = t('Recent comments');
277
    $block['content'] = theme('comment_block');
Dries's avatar
   
Dries committed
278
279
280
281
    return $block;
  }
}

282
283
284
285
286
287
288
289
290
291
/**
 * 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,
Dries's avatar
Dries committed
292
 *   subject, cid, and timestamp, or an empty array if there are no recent
293
294
295
296
297
298
 *   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.
299
  $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);
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324

  $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
 */
325
326
function theme_comment_block() {
  $items = array();
327
  foreach (comment_get_recent() as $comment) {
328
    $items[] = l($comment->subject, 'node/'. $comment->nid, array('fragment' => 'comment-'. $comment->cid)) .'<br />'. t('@time ago', array('@time' => format_interval(time() - $comment->timestamp)));
329
  }
330
331
332
  if ($items) {
    return theme('item_list', $items);
  }
333
334
}

Dries's avatar
   
Dries committed
335
336
337
/**
 * Implementation of hook_link().
 */
338
function comment_link($type, $node = NULL, $teaser = FALSE) {
Dries's avatar
   
Dries committed
339
340
341
342
  $links = array();

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

343
    if ($teaser) {
Dries's avatar
   
Dries committed
344
345
346
347
348
349
      // Main page: display the number of comments that have been posted.

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

        if ($all) {
350
          $links['comment_comments'] = array(
351
            'title' => format_plural($all, '1 comment', '@count comments'),
352
353
            'href' => "node/$node->nid",
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
354
            'fragment' => 'comments'
355
          );
356
357

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

          if ($new) {
360
            $links['comment_new_comments'] = array(
361
              'title' => format_plural($new, '1 new comment', '@count new comments'),
362
363
364
              'href' => "node/$node->nid",
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
              'fragment' => 'new'
365
            );
Dries's avatar
   
Dries committed
366
367
368
          }
        }
        else {
Dries's avatar
Dries committed
369
          if ($node->comment == COMMENT_NODE_READ_WRITE) {
Dries's avatar
   
Dries committed
370
            if (user_access('post comments')) {
371
              $links['comment_add'] = array(
372
                'title' => t('Add new comment'),
373
374
                'href' => "comment/reply/$node->nid",
                'attributes' => array('title' => t('Add a new comment to this page.')),
375
                'fragment' => 'comment-form'
376
              );
Dries's avatar
   
Dries committed
377
378
            }
            else {
379
              $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node->nid);
Dries's avatar
   
Dries committed
380
381
382
383
384
385
386
387
388
            }
          }
        }
      }
    }
    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

389
      if ($node->comment == COMMENT_NODE_READ_WRITE) {
Dries's avatar
   
Dries committed
390
        if (user_access('post comments')) {
391
          if (variable_get('comment_form_location', COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_SEPARATE_PAGE) {
392
            $links['comment_add'] = array(
393
              'title' => t('Add new comment'),
394
395
              'href' => "comment/reply/$node->nid",
              'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
396
              'fragment' => 'comment-form'
397
            );
398
          }
Dries's avatar
   
Dries committed
399
400
        }
        else {
401
          $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node->nid);
Dries's avatar
   
Dries committed
402
403
404
405
406
407
        }
      }
    }
  }

  if ($type == 'comment') {
408
    $links = comment_links($node, $teaser);
Dries's avatar
   
Dries committed
409
  }
410
411
412
  if (isset($links['comment_forbidden'])) {
    $links['comment_forbidden']['html'] = TRUE;
  }
Dries's avatar
   
Dries committed
413
414
415
416

  return $links;
}

417
function comment_form_alter(&$form, $form_state, $form_id) {
418
419
420
421
  if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
    $form['workflow']['comment'] = array(
      '#type' => 'radios',
      '#title' => t('Default comment setting'),
422
      '#default_value' => variable_get('comment_'. $form['#node_type']->type, COMMENT_NODE_READ_WRITE),
423
424
425
426
      '#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.'),
    );
  }
427
  elseif (isset($form['type']) && isset($form['#node'])) {
428
    if ($form['type']['#value'] .'_node_form' == $form_id) {
429
      $node = $form['#node'];
430
431
432
433
434
435
436
437
438
439
440
441
442
443
      $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')),
      );
444
    }
445
446
447
  }
}

Dries's avatar
   
Dries committed
448
449
/**
 * Implementation of hook_nodeapi().
Dries's avatar
   
Dries committed
450
 *
Dries's avatar
   
Dries committed
451
452
453
 */
function comment_nodeapi(&$node, $op, $arg = 0) {
  switch ($op) {
Dries's avatar
   
Dries committed
454
    case 'load':
455
      return db_fetch_array(db_query("SELECT last_comment_timestamp, last_comment_name, comment_count FROM {node_comment_statistics} WHERE nid = %d", $node->nid));
456
457
458
459
      break;

    case 'prepare':
      if (!isset($node->comment)) {
Dries's avatar
Dries committed
460
        $node->comment = variable_get("comment_$node->type", COMMENT_NODE_READ_WRITE);
Dries's avatar
   
Dries committed
461
462
      }
      break;
463

464
    case 'insert':
465
      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->changed, $node->uid);
466
467
468
      break;

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

Dries's avatar
Dries committed
473
474
    case 'update index':
      $text = '';
475
      $comments = db_query('SELECT subject, comment, format FROM {comments} WHERE nid = %d AND status = %d', $node->nid, COMMENT_PUBLISHED);
Dries's avatar
Dries committed
476
      while ($comment = db_fetch_object($comments)) {
477
        $text .= '<h2>'. check_plain($comment->subject) .'</h2>'. check_markup($comment->comment, $comment->format, FALSE);
Dries's avatar
Dries committed
478
479
      }
      return $text;
480

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

Steven Wittens's avatar
- Typo    
Steven Wittens committed
485
    case 'rss item':
486
      if ($node->comment != COMMENT_NODE_DISABLED) {
487
        return array(array('key' => 'comments', 'value' => url('node/'. $node->nid, array('fragment' => 'comments', 'absolute' => TRUE))));
488
489
490
491
      }
      else {
        return array();
      }
Dries's avatar
   
Dries committed
492
493
494
495
496
497
498
  }
}

/**
 * Implementation of hook_user().
 */
function comment_user($type, $edit, &$user, $category = NULL) {
499
  if ($type == 'delete') {
500
501
502
    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
503
504
}

505
/**
Dries's avatar
   
Dries committed
506
 * Menu callback; presents the comment settings page.
507
 */
Dries's avatar
Dries committed
508
function comment_admin_settings() {
509
510
  $form['viewing_options'] = array(
    '#type' => 'fieldset',
511
    '#title' => t('Viewing options'),
512
513
    '#collapsible' => TRUE,
  );
Dries's avatar
   
Dries committed
514

Dries's avatar
Dries committed
515
516
517
518
519
  $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(),
520
    '#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
521
  );
522

Dries's avatar
Dries committed
523
524
525
  $form['viewing_options']['comment_default_order'] = array(
    '#type' => 'radios',
    '#title' => t('Default display order'),
526
    '#default_value' => variable_get('comment_default_order', COMMENT_ORDER_NEWEST_FIRST),
Dries's avatar
Dries committed
527
    '#options' => _comment_get_orders(),
528
    '#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
529
  );
Dries's avatar
   
Dries committed
530

531
  $form['viewing_options']['comment_default_per_page'] = array(
532
533
534
535
    '#type' => 'select',
    '#title' => t('Default comments per page'),
    '#default_value' => variable_get('comment_default_per_page', 50),
    '#options' => _comment_per_page(),
536
    '#description' => t('Default number of comments for each page: more comments are distributed in several pages.'),
537
538
  );

Dries's avatar
Dries committed
539
540
541
542
543
544
545
546
547
  $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')),
548
    '#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
549
  );
550

551
552
  $form['posting_settings'] = array(
    '#type' => 'fieldset',
553
    '#title' => t('Posting settings'),
554
555
    '#collapsible' => TRUE,
  );
556

Dries's avatar
Dries committed
557
558
  $form['posting_settings']['comment_anonymous'] = array(
    '#type' => 'radios',
559
    '#title' => t('Anonymous commenting'),
Dries's avatar
Dries committed
560
561
562
563
564
    '#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')),
565
    '#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
566
  );
567
  if (!user_access('post comments', user_load(array('uid' => 0)))) {
568
    $form['posting_settings']['comment_anonymous']['#disabled'] = TRUE;
569
  }
570
571

  $form['posting_settings']['comment_subject_field'] = array(
572
573
574
575
    '#type' => 'radios',
    '#title' => t('Comment subject field'),
    '#default_value' => variable_get('comment_subject_field', 1),
    '#options' => array(t('Disabled'), t('Enabled')),
576
    '#description' => t('Can users provide a unique subject for their comments?'),
577
578
  );

579
580
581
582
583
584
  $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')),
  );
585

Dries's avatar
Dries committed
586
587
588
589
  $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),
590
    '#options' => array(t('Display on separate page'), t('Display below post or comments')),
Dries's avatar
Dries committed
591
  );
592

593
  return system_settings_form($form);
Dries's avatar
   
Dries committed
594
595
}

596
597
598
599
600
601
/**
 * 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
602
 * statements based on the replies to their posts.
603
 */
Dries's avatar
   
Dries committed
604
function comment_access($op, $comment) {
Dries's avatar
   
Dries committed
605
606
  global $user;

607
  if ($op == 'edit') {
608
    return ($user->uid && $user->uid == $comment->uid && comment_num_replies($comment->cid) == 0) || user_access('administer comments');
Dries's avatar
   
Dries committed
609
610
  }
}
611

Dries's avatar
   
Dries committed
612
function comment_node_url() {
Dries's avatar
Dries committed
613
  return arg(0) .'/'. arg(1);
Dries's avatar
   
Dries committed
614
}
Dries's avatar
   
Dries committed
615

Dries's avatar
   
Dries committed
616
617
618
function comment_edit($cid) {
  global $user;

619
  $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
620
  $comment = drupal_unpack($comment);
621
  $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
622
  if (comment_access('edit', $comment)) {
623
    return comment_form_box((array)$comment);
624
625
626
  }
  else {
    drupal_access_denied();
Dries's avatar
   
Dries committed
627
628
629
  }
}

630
631
632
633
634
635
636
637
638
639
640
/**
 * 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.
 *
641
642
 * @param $node
 *   Every comment belongs to a node. This is that node.
643
644
645
646
647
648
649
 * @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.
 */
650
function comment_reply($node, $pid = NULL) {
651
  // Set the breadcrumb trail.
652
  menu_set_location(array(array('path' => "node/$node->nid", 'title' => $node->title), array('path' => "comment/reply/$node->nid")));
Dries's avatar
   
Dries committed
653

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

656
  $output = '';
Dries's avatar
   
Dries committed
657

Dries's avatar
Dries committed
658
  if (user_access('access comments')) {
659
    // The user is previewing a comment prior to submitting it.
660
661
    if ($op == t('Preview comment')) {
      if (user_access('post comments')) {
662
        $output .= comment_form_box(array('pid' => $pid, 'nid' => $node->nid), NULL);
663
664
665
      }
      else {
        drupal_set_message(t('You are not authorized to post comments.'), 'error');
666
        drupal_goto("node/$node->nid");
667
      }
Dries's avatar
   
Dries committed
668
669
    }
    else {
670
      // $pid indicates that this is a reply to a comment.
671
      if ($pid) {
672
        // load the comment whose cid = $pid
673
        if ($comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.signature, 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))) {
674
675
          // If that comment exists, make sure that the current comment and the parent comment both
          // belong to the same parent node.
676
          if ($comment->nid != $node->nid) {
677
678
            // 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');
679
            drupal_goto("node/$node->nid");
680
          }
681
          // Display the parent comment
682
683
          $comment = drupal_unpack($comment);
          $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
684
          $output .= theme('comment_view', $comment, $node);
685
686
687
        }
        else {
          drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
688
          drupal_goto("node/$node->nid");
689
        }
690
      }
691
      // This is the case where the comment is in response to a node. Display the node.
692
693
694
695
      else if (user_access('access content')) {
        $output .= node_view($node);
      }

696
      // Should we show the reply box?
697
      if (node_comment_mode($node->nid) != COMMENT_NODE_READ_WRITE) {
698
        drupal_set_message(t("This discussion is closed: you can't post new comments."), 'error');
699
        drupal_goto("node/$node->nid");
700
701
      }
      else if (user_access('post comments')) {
702
        $output .= comment_form_box(array('pid' => $pid, 'nid' => $node->nid), t('Reply'));
703
704
705
      }
      else {
        drupal_set_message(t('You are not authorized to post comments.'), 'error');
706
        drupal_goto("node/$node->nid");
707
      }
Dries's avatar
   
Dries committed
708
    }
Kjartan's avatar
Kjartan committed
709
710
  }
  else {
711
    drupal_set_message(t('You are not authorized to view comments.'), 'error');
712
    drupal_goto("node/$node->nid");
Dries's avatar
   
Dries committed
713
  }
Dries's avatar
   
Dries committed
714

Dries's avatar
   
Dries committed
715
  return $output;
Dries's avatar
   
Dries committed
716
717
}

718
719
720
721
722
723
724
/**
 * Accepts a submission of new or changed comment content.
 *
 * @param $edit
 *   A comment array.
 *
 * @return
725
 *   If the comment is successfully saved the comment ID is returned. If the comment
726
727
728
 *   is not saved, FALSE is returned.
 */
function comment_save($edit) {
Dries's avatar
   
Dries committed
729
  global $user;
730
  if (user_access('post comments') && (user_access('administer comments') || node_comment_mode($edit['nid']) == COMMENT_NODE_READ_WRITE)) {
Dries's avatar
   
Dries committed
731
    if (!form_get_errors()) {
732
      if ($edit['cid']) {
733
        // Update the comment in the database.
734
        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
735

736
        // Allow modules to respond to the updating of a comment.
737
738
        comment_invoke_comment($edit, 'update');

Dries's avatar
Dries committed
739
        // Add an entry to the watchdog log.
740
        watchdog('content', '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
741
742
      }
      else {
743
        // Add the comment to database.
744
        $status = user_access('post comments without approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
Dries's avatar
   
Dries committed
745

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

753
754
          // Strip the "/" from the end of the thread.
          $max = rtrim($max, '/');
Dries's avatar
   
Dries committed
755

756
          // Finally, build the thread field for this new comment.
757
          $thread = int2vancode(vancode2int($max) + 1) .'/';
Dries's avatar
   
Dries committed
758
759
        }
        else {
760
761
          // This is comment with a parent comment: we increase
          // the part of the thread value at the proper depth.
Dries's avatar
   
Dries committed
762
763

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

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

769
          // Get the max value in _this_ thread.
Dries's avatar
   
Dries committed
770
          $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
771

772
773
          if ($max == '') {
            // First child of this parent.
774
            $thread = $parent->thread .'.'. int2vancode(0) .'/';
Dries's avatar
   
Dries committed
775
776
          }
          else {
777
778
            // Strip the "/" at the end of the thread.
            $max = rtrim($max, '/');
Dries's avatar
   
Dries committed
779

780
781
782
            // We need to get the value at the correct depth.
            $parts = explode('.', $max);
            $parent_depth = count(explode('.', $parent->thread));
Dries's avatar
   
Dries committed
783
784
            $last = $parts[$parent_depth];

785
            // Finally, build the thread field for this new comment.
786
            $thread = $parent->thread .'.'. int2vancode(vancode2int($last) + 1) .'/';
Dries's avatar
   
Dries committed
787
788
789
          }
        }

Dries's avatar
   
Dries committed
790
791
        $edit['timestamp'] = time();

792
        if ($edit['uid'] === $user->uid) { // '===' because we want to modify anonymous users too
Dries's avatar
   
Dries committed
793
794
795
          $edit['name'] = $user->name;
        }

796
        $edit += array('mail' => '', 'homepage' => '');
797
        db_query("INSERT INTO {comments} (nid, pid, uid, subject, comment, format, hostname, timestamp, status, thread, name, mail, homepage) VALUES (%d, %d, %d, '%s', '%s', %d, '%s', %d, %d, '%s', '%s', '%s', '%s')", $edit['nid'], $edit['pid'], $edit['uid'], $edit['subject'], $edit['comment'], $edit['format'], ip_address(), $edit['timestamp'], $status, $thread, $edit['name'], $edit['mail'], $edit['homepage']);
798
        $edit['cid'] = db_last_insert_id('comments', 'cid');
Dries's avatar
   
Dries committed
799

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

803
        // Add an entry to the watchdog log.
804
        watchdog('content', '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
805
      }
806
      _comment_update_node_statistics($edit['nid']);
Dries's avatar
   
Dries committed
807

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

Dries's avatar
   
Dries committed
811
      // Explain the approval queue if necessary, and then
Dries's avatar
   
Dries committed
812
      // redirect the user to the node he's commenting on.
813
      if ($status == COMMENT_NOT_PUBLISHED) {
Dries's avatar
   
Dries committed
814
        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
815
      }
816
817
818
      else {
	comment_invoke_comment($edit, 'publish');
      }
819
      return $edit['cid'];
Dries's avatar
   
Dries committed
820
821
    }
    else {
822
      return FALSE;
Dries's avatar
   
Dries committed
823
824
    }
  }
Dries's avatar
   
Dries committed
825
  else {
826
827
    watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed node %subject.', array('%subject' => $edit['subject']), WATCHDOG_WARNING);
    drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed node %subject.', array('%subject' => $edit['subject'])), 'error');
828
    return FALSE;
Dries's avatar
   
Dries committed
829
830
831
832
  }
}

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

Dries's avatar
   
Dries committed
835
  $links = array();
Dries's avatar
   
Dries committed
836

837
  // If we are viewing just this comment, we link back to the node.
Dries's avatar
   
Dries committed
838
  if ($return) {
839
    $links['comment_parent'] = array(
840
841
842
      'title' => t('parent'),
      'href' => comment_node_url(),
      'fragment' => "comment-$comment->cid"
843
    );
Dries's avatar
   
Dries committed
844
  }
Dries's avatar
   
Dries committed
845

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

Dries's avatar
   
Dries committed
878
  return $links;
Dries's avatar
  &