comment.module 81 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/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
  }
Dries's avatar
   
Dries committed
138
139
}

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

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

204
  // Tabs:
205
  $items['admin/content/comment/new'] = array(
206
    'title' => 'Published comments',
207
208
209
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
210
  $items['admin/content/comment/approval'] = array(
211
    'title' => 'Approval queue',
212
213
214
215
216
    'page arguments' => array('approval'),
    'type' => MENU_LOCAL_TASK,
  );

  $items['comment/delete'] = array(
217
    'title' => 'Delete comment',
218
    'page callback' => 'comment_delete',
219
220
221
222
223
    'access arguments' => array('administer comments'),
    'type' => MENU_CALLBACK,
  );

  $items['comment/edit'] = array(
224
    'title' => 'Edit comment',
225
226
227
228
    'page callback' => 'comment_edit',
    'access arguments' => array('post comments'),
    'type' => MENU_CALLBACK,
  );
229
  $items['comment/reply/%node'] = array(
230
    'title' => 'Reply to comment',
231
    'page callback' => 'comment_reply',
232
    'page arguments' => array(2),
233
234
235
236
    'access callback' => 'node_access',
    'access arguments' => array('view', 2),
    'type' => MENU_CALLBACK,
  );
Dries's avatar
   
Dries committed
237
238
239
240

  return $items;
}

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
/**
 * Implementation of hook_node_type().
 *
 */
function comment_node_type($op, $info) {
  $settings = array(
    'comment',
    'comment_default_mode',
    'comment_default_order',
    'comment_default_per_page',
    'comment_controls',
    'comment_anonymous',
    'comment_subject_field',
    'comment_preview',
    'comment_form_location',
  );
  switch ($op) {
    case 'delete':
      foreach ($settings as $setting) {
        variable_del($setting .'_'. $info->type);
      }
      break;
  }
}

Dries's avatar
   
Dries committed
266
267
268
269
/**
 * Implementation of hook_perm().
 */
function comment_perm() {
270
  return array('access comments', 'post comments', 'administer comments', 'post comments without approval');
Dries's avatar
   
Dries committed
271
272
273
274
275
276
277
278
279
280
281
282
}

/**
 * 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;
  }
283
  else if ($op == 'view' && user_access('access comments')) {
Dries's avatar
   
Dries committed
284
    $block['subject'] = t('Recent comments');
285
    $block['content'] = theme('comment_block');
Dries's avatar
   
Dries committed
286
287
288
289
    return $block;
  }
}

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

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

327
328
329
/**
 * Calculate page number for first new comment.
 */
330
331
332
333
function comment_new_page_count($num_comments, $new_replies, $node) {
  $comments_per_page = _comment_get_display_setting('comments_per_page', $node);
  $mode = _comment_get_display_setting('mode', $node);
  $order = _comment_get_display_setting('sort', $node);
334
  $pagenum = NULL;
335
  $flat = in_array($mode, array(COMMENT_MODE_FLAT_COLLAPSED, COMMENT_MODE_FLAT_EXPANDED));
336
337
338
339
  if ($num_comments <= $comments_per_page || ($flat && $order == COMMENT_ORDER_NEWEST_FIRST)) {
    // Only one page of comments or flat forum and newest first.
    // First new comment will always be on first page.
    $pageno = 0;
340
  }
341
  else {
342
    if ($flat) {
343
344
      // Flat comments and oldest first.
      $count = $num_comments - $new_replies;
345
    }
346
347
    else {
      // Threaded comments. See the documentation for comment_render().
348
      if ($order == COMMENT_ORDER_NEWEST_FIRST) {
349
        // Newest first: find the last thread with new comment
350
        $result = db_query('(SELECT thread FROM {comments} WHERE nid = %d  AND status = 0 ORDER BY timestamp DESC LIMIT %d) ORDER BY thread DESC LIMIT 1', $node->nid, $new_replies);
351
        $thread = db_result($result);
352
        $result_count = db_query("SELECT COUNT(*) FROM {comments} WHERE nid = %d AND status = 0 AND thread > '". $thread ."'", $node->nid);
353
354
      }
      else {
355
        // Oldest first: find the first thread with new comment
356
        $result = db_query('(SELECT thread FROM {comments} WHERE nid = %d  AND status = 0 ORDER BY timestamp DESC LIMIT %d) ORDER BY SUBSTRING(thread, 1, (LENGTH(thread) - 1)) LIMIT 1', $node->nid, $new_replies);
357
        $thread = substr(db_result($result), 0, -1);
358
        $result_count = db_query("SELECT COUNT(*) FROM {comments} WHERE nid = %d AND status = 0 AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < '". $thread ."'", $node->nid);
359
360
361
362
363
364
365
366
367
368
369
      }
      $count = db_result($result_count);
    }
    $pageno =  $count / $comments_per_page;
  }
  if ($pageno >= 1) {
    $pagenum = "page=" . intval($pageno);
  }
  return $pagenum;
}

370
371
372
373
374
375
/**
 * Returns a formatted list of recent comments to be displayed in the comment
 * block.
 *
 * @ingroup themeable
 */
376
377
function theme_comment_block() {
  $items = array();
378
  foreach (comment_get_recent() as $comment) {
379
    $items[] = l($comment->subject, 'node/'. $comment->nid, array('fragment' => 'comment-'. $comment->cid)) .'<br />'. t('@time ago', array('@time' => format_interval(time() - $comment->timestamp)));
380
  }
381
382
383
  if ($items) {
    return theme('item_list', $items);
  }
384
385
}

Dries's avatar
   
Dries committed
386
387
388
/**
 * Implementation of hook_link().
 */
389
function comment_link($type, $node = NULL, $teaser = FALSE) {
Dries's avatar
   
Dries committed
390
391
392
393
  $links = array();

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

394
    if ($teaser) {
Dries's avatar
   
Dries committed
395
396
397
398
399
400
      // Main page: display the number of comments that have been posted.

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

        if ($all) {
401
          $links['comment_comments'] = array(
402
            'title' => format_plural($all, '1 comment', '@count comments'),
403
404
            'href' => "node/$node->nid",
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
405
            'fragment' => 'comments'
406
          );
407
408

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

          if ($new) {
411
            $links['comment_new_comments'] = array(
412
              'title' => format_plural($new, '1 new comment', '@count new comments'),
413
              'href' => "node/$node->nid",
414
              'query' => comment_new_page_count($all, $new, $node),
415
416
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
              'fragment' => 'new'
417
            );
Dries's avatar
   
Dries committed
418
419
420
          }
        }
        else {
Dries's avatar
Dries committed
421
          if ($node->comment == COMMENT_NODE_READ_WRITE) {
Dries's avatar
   
Dries committed
422
            if (user_access('post comments')) {
423
              $links['comment_add'] = array(
424
                'title' => t('Add new comment'),
425
426
                'href' => "comment/reply/$node->nid",
                'attributes' => array('title' => t('Add a new comment to this page.')),
427
                'fragment' => 'comment-form'
428
              );
Dries's avatar
   
Dries committed
429
430
            }
            else {
431
              $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node);
Dries's avatar
   
Dries committed
432
433
434
435
436
437
438
439
440
            }
          }
        }
      }
    }
    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

441
      if ($node->comment == COMMENT_NODE_READ_WRITE) {
Dries's avatar
   
Dries committed
442
        if (user_access('post comments')) {
443
          if (variable_get('comment_form_location_'. $node->type, COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_SEPARATE_PAGE) {
444
            $links['comment_add'] = array(
445
              'title' => t('Add new comment'),
446
447
              'href' => "comment/reply/$node->nid",
              'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
448
              'fragment' => 'comment-form'
449
            );
450
          }
Dries's avatar
   
Dries committed
451
452
        }
        else {
453
          $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node);
Dries's avatar
   
Dries committed
454
455
456
457
458
459
        }
      }
    }
  }

  if ($type == 'comment') {
460
    $links = comment_links($node, $teaser);
Dries's avatar
   
Dries committed
461
  }
462
463
464
  if (isset($links['comment_forbidden'])) {
    $links['comment_forbidden']['html'] = TRUE;
  }
Dries's avatar
   
Dries committed
465
466
467
468

  return $links;
}

469
function comment_form_alter(&$form, $form_state, $form_id) {
470
  if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
471
472
473
474
    $form['comment'] = array(
      '#type' => 'fieldset',
      '#title' => t('Comment settings'),
      '#collapsible' => TRUE,
475
      '#collapsed' => TRUE,
476
477
    );
    $form['comment']['comment'] = array(
478
479
      '#type' => 'radios',
      '#title' => t('Default comment setting'),
480
      '#default_value' => variable_get('comment_'. $form['#node_type']->type, COMMENT_NODE_READ_WRITE),
481
482
483
      '#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.'),
    );
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
    $form['comment']['comment_default_mode'] = array(
      '#type' => 'radios',
      '#title' => t('Default display mode'),
      '#default_value' => variable_get('comment_default_mode_'. $form['#node_type']->type, COMMENT_MODE_THREADED_EXPANDED),
      '#options' => _comment_get_modes(),
      '#description' => t('The default view for comments. Expanded views display the body of the comment. Threaded views keep replies together.'),
    );
    $form['comment']['comment_default_order'] = array(
      '#type' => 'radios',
      '#title' => t('Default display order'),
      '#default_value' => variable_get('comment_default_order_'. $form['#node_type']->type, COMMENT_ORDER_NEWEST_FIRST),
      '#options' => _comment_get_orders(),
      '#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.'),
    );
    $form['comment']['comment_default_per_page'] = array(
      '#type' => 'select',
      '#title' => t('Default comments per page'),
      '#default_value' => variable_get('comment_default_per_page_'. $form['#node_type']->type, 50),
      '#options' => _comment_per_page(),
      '#description' => t('Default number of comments for each page: more comments are distributed in several pages.'),
    );
    $form['comment']['comment_controls'] = array(
      '#type' => 'radios',
      '#title' => t('Comment controls'),
      '#default_value' => variable_get('comment_controls_'. $form['#node_type']->type, 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')),
      '#description' => t('Position of the comment controls box. The comment controls let the user change the default display mode and display order of comments.'),
    );
    $form['comment']['comment_anonymous'] = array(
      '#type' => 'radios',
      '#title' => t('Anonymous commenting'),
      '#default_value' => variable_get('comment_anonymous_'. $form['#node_type']->type, 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')),
524
      '#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/permissions', array('fragment' => 'module-comment')))),
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
    );
    if (!user_access('post comments', user_load(array('uid' => 0)))) {
      $form['comment']['comment_anonymous']['#disabled'] = TRUE;
    }
    $form['comment']['comment_subject_field'] = array(
      '#type' => 'radios',
      '#title' => t('Comment subject field'),
      '#default_value' => variable_get('comment_subject_field_'. $form['#node_type']->type, 1),
      '#options' => array(t('Disabled'), t('Enabled')),
      '#description' => t('Can users provide a unique subject for their comments?'),
    );
    $form['comment']['comment_preview'] = array(
      '#type' => 'radios',
      '#title' => t('Preview comment'),
      '#default_value' => variable_get('comment_preview_'. $form['#node_type']->type, COMMENT_PREVIEW_REQUIRED),
      '#options' => array(t('Optional'), t('Required')),
      '#description' => t("Forces a user to look at their comment by clicking on a 'Preview' button before they can actually add the comment"),
    );
    $form['comment']['comment_form_location'] = array(
      '#type' => 'radios',
      '#title' => t('Location of comment submission form'),
      '#default_value' => variable_get('comment_form_location_'. $form['#node_type']->type, COMMENT_FORM_SEPARATE_PAGE),
      '#options' => array(t('Display on separate page'), t('Display below post or comments')),
    );
549
  }
550
  elseif (isset($form['type']) && isset($form['#node'])) {
551
    if ($form['type']['#value'] .'_node_form' == $form_id) {
552
      $node = $form['#node'];
553
554
555
556
557
558
559
560
561
562
563
564
565
566
      $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')),
      );
567
    }
568
569
570
  }
}

Dries's avatar
   
Dries committed
571
572
/**
 * Implementation of hook_nodeapi().
Dries's avatar
   
Dries committed
573
 *
Dries's avatar
   
Dries committed
574
575
576
 */
function comment_nodeapi(&$node, $op, $arg = 0) {
  switch ($op) {
Dries's avatar
   
Dries committed
577
    case 'load':
578
      return db_fetch_array(db_query("SELECT last_comment_timestamp, last_comment_name, comment_count FROM {node_comment_statistics} WHERE nid = %d", $node->nid));
579
580
581
582
      break;

    case 'prepare':
      if (!isset($node->comment)) {
Dries's avatar
Dries committed
583
        $node->comment = variable_get("comment_$node->type", COMMENT_NODE_READ_WRITE);
Dries's avatar
   
Dries committed
584
585
      }
      break;
586

587
    case 'insert':
588
      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);
589
590
591
      break;

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

Dries's avatar
Dries committed
596
597
    case 'update index':
      $text = '';
598
      $comments = db_query('SELECT subject, comment, format FROM {comments} WHERE nid = %d AND status = %d', $node->nid, COMMENT_PUBLISHED);
Dries's avatar
Dries committed
599
      while ($comment = db_fetch_object($comments)) {
600
        $text .= '<h2>'. check_plain($comment->subject) .'</h2>'. check_markup($comment->comment, $comment->format, FALSE);
Dries's avatar
Dries committed
601
602
      }
      return $text;
603

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

Steven Wittens's avatar
- Typo    
Steven Wittens committed
608
    case 'rss item':
609
      if ($node->comment != COMMENT_NODE_DISABLED) {
610
        return array(array('key' => 'comments', 'value' => url('node/'. $node->nid, array('fragment' => 'comments', 'absolute' => TRUE))));
611
612
613
614
      }
      else {
        return array();
      }
Dries's avatar
   
Dries committed
615
616
617
618
619
620
621
  }
}

/**
 * Implementation of hook_user().
 */
function comment_user($type, $edit, &$user, $category = NULL) {
622
  if ($type == 'delete') {
623
624
625
    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
626
627
}

628
629
630
631
632
633
/**
 * 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
634
 * statements based on the replies to their posts.
635
 */
Dries's avatar
   
Dries committed
636
function comment_access($op, $comment) {
Dries's avatar
   
Dries committed
637
638
  global $user;

639
  if ($op == 'edit') {
640
    return ($user->uid && $user->uid == $comment->uid && comment_num_replies($comment->cid) == 0) || user_access('administer comments');
Dries's avatar
   
Dries committed
641
642
  }
}
643

Dries's avatar
   
Dries committed
644
function comment_node_url() {
Dries's avatar
Dries committed
645
  return arg(0) .'/'. arg(1);
Dries's avatar
   
Dries committed
646
}
Dries's avatar
   
Dries committed
647

Dries's avatar
   
Dries committed
648
649
650
function comment_edit($cid) {
  global $user;

651
  $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
652
  $comment = drupal_unpack($comment);
653
  $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
654
  if (comment_access('edit', $comment)) {
655
    return comment_form_box((array)$comment);
656
657
658
  }
  else {
    drupal_access_denied();
Dries's avatar
   
Dries committed
659
660
661
  }
}

662
663
664
665
666
667
668
669
670
671
672
/**
 * 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.
 *
673
674
 * @param $node
 *   Every comment belongs to a node. This is that node.
675
676
677
678
679
680
681
 * @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.
 */
682
function comment_reply($node, $pid = NULL) {
683
  // Set the breadcrumb trail.
684
  menu_set_location(array(array('path' => "node/$node->nid", 'title' => $node->title), array('path' => "comment/reply/$node->nid")));
Dries's avatar
   
Dries committed
685

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

688
  $output = '';
Dries's avatar
   
Dries committed
689

Dries's avatar
Dries committed
690
  if (user_access('access comments')) {
691
    // The user is previewing a comment prior to submitting it.
692
693
    if ($op == t('Preview comment')) {
      if (user_access('post comments')) {
694
        $output .= comment_form_box(array('pid' => $pid, 'nid' => $node->nid), NULL);
695
696
697
      }
      else {
        drupal_set_message(t('You are not authorized to post comments.'), 'error');
698
        drupal_goto("node/$node->nid");
699
      }
Dries's avatar
   
Dries committed
700
701
    }
    else {
702
      // $pid indicates that this is a reply to a comment.
703
      if ($pid) {
704
        // load the comment whose cid = $pid
705
        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))) {
706
707
          // If that comment exists, make sure that the current comment and the parent comment both
          // belong to the same parent node.
708
          if ($comment->nid != $node->nid) {
709
710
            // 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');
711
            drupal_goto("node/$node->nid");
712
          }
713
          // Display the parent comment
714
715
          $comment = drupal_unpack($comment);
          $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
716
          $output .= theme('comment_view', $comment, $node);
717
718
719
        }
        else {
          drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
720
          drupal_goto("node/$node->nid");
721
        }
722
      }
723
      // This is the case where the comment is in response to a node. Display the node.
724
725
726
727
      else if (user_access('access content')) {
        $output .= node_view($node);
      }

728
      // Should we show the reply box?
729
      if (node_comment_mode($node->nid) != COMMENT_NODE_READ_WRITE) {
730
        drupal_set_message(t("This discussion is closed: you can't post new comments."), 'error');
731
        drupal_goto("node/$node->nid");
732
733
      }
      else if (user_access('post comments')) {
734
        $output .= comment_form_box(array('pid' => $pid, 'nid' => $node->nid), t('Reply'));
735
736
737
      }
      else {
        drupal_set_message(t('You are not authorized to post comments.'), 'error');
738
        drupal_goto("node/$node->nid");
739
      }
Dries's avatar
   
Dries committed
740
    }
Kjartan's avatar
Kjartan committed
741
742
  }
  else {
743
    drupal_set_message(t('You are not authorized to view comments.'), 'error');
744
    drupal_goto("node/$node->nid");
Dries's avatar
   
Dries committed
745
  }
Dries's avatar
   
Dries committed
746

Dries's avatar
   
Dries committed
747
  return $output;
Dries's avatar
   
Dries committed
748
749
}

750
751
752
753
754
755
756
/**
 * Accepts a submission of new or changed comment content.
 *
 * @param $edit
 *   A comment array.
 *
 * @return
757
 *   If the comment is successfully saved the comment ID is returned. If the comment
758
759
760
 *   is not saved, FALSE is returned.
 */
function comment_save($edit) {
Dries's avatar
   
Dries committed
761
  global $user;
762
  if (user_access('post comments') && (user_access('administer comments') || node_comment_mode($edit['nid']) == COMMENT_NODE_READ_WRITE)) {
Dries's avatar
   
Dries committed
763
    if (!form_get_errors()) {
764
765
766
767
768
769
      $edit += array(
        'mail' => '',
        'homepage' => '',
        'name' => '',
        'status' => user_access('post comments without approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED,
      );
770
      if ($edit['cid']) {
771
        // Update the comment in the database.
772
        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
773

774
        // Allow modules to respond to the updating of a comment.
775
776
        comment_invoke_comment($edit, 'update');

Dries's avatar
Dries committed
777
        // Add an entry to the watchdog log.
778
        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
779
780
      }
      else {
781
        // Add the comment to database.
782
783
        // Here we are building the thread field. See the documentation for
        // comment_render().
784
        if ($edit['pid'] == 0) {
785
786
          // This is a comment with no parent comment (depth 0): we start
          // by retrieving the maximum thread level.
787
          $max = db_result(db_query('SELECT MAX(thread) FROM {comments} WHERE nid = %d', $edit['nid']));
Dries's avatar
   
Dries committed
788

789
790
          // Strip the "/" from the end of the thread.
          $max = rtrim($max, '/');
Dries's avatar
   
Dries committed
791

792
          // Finally, build the thread field for this new comment.
793
          $thread = int2vancode(vancode2int($max) + 1) .'/';
Dries's avatar
   
Dries committed
794
795
        }
        else {
796
797
          // This is comment with a parent comment: we increase
          // the part of the thread value at the proper depth.
Dries's avatar
   
Dries committed
798
799

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

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

805
          // Get the max value in _this_ thread.
Dries's avatar
   
Dries committed
806
          $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
807

808
809
          if ($max == '') {
            // First child of this parent.
810
            $thread = $parent->thread .'.'. int2vancode(0) .'/';
Dries's avatar
   
Dries committed
811
812
          }
          else {
813
814
            // Strip the "/" at the end of the thread.
            $max = rtrim($max, '/');
Dries's avatar
   
Dries committed
815

816
817
818
            // We need to get the value at the correct depth.
            $parts = explode('.', $max);
            $parent_depth = count(explode('.', $parent->thread));
Dries's avatar
   
Dries committed
819
820
            $last = $parts[$parent_depth];

821
            // Finally, build the thread field for this new comment.
822
            $thread = $parent->thread .'.'. int2vancode(vancode2int($last) + 1) .'/';
Dries's avatar
   
Dries committed
823
824
825
          }
        }

Dries's avatar
   
Dries committed
826
827
        $edit['timestamp'] = time();

828
        if ($edit['uid'] === $user->uid) { // '===' because we want to modify anonymous users too
Dries's avatar
   
Dries committed
829
830
          $edit['name'] = $user->name;
        }
831

832
        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'], $edit['status'], $thread, $edit['name'], $edit['mail'], $edit['homepage']);
833
        $edit['cid'] = db_last_insert_id('comments', 'cid');
Dries's avatar
   
Dries committed
834

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

838
        // Add an entry to the watchdog log.
839
        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
840
      }
841
      _comment_update_node_statistics($edit['nid']);
Dries's avatar
   
Dries committed
842

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

Dries's avatar
   
Dries committed
846
      // Explain the approval queue if necessary, and then
Dries's avatar
   
Dries committed
847
      // redirect the user to the node he's commenting on.
848
      if ($edit['status'] == COMMENT_NOT_PUBLISHED) {
Dries's avatar
   
Dries committed
849
        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
850
      }
851
      else {
852
        comment_invoke_comment($edit, 'publish');
853
      }
854
      return $edit['cid'];
Dries's avatar
   
Dries committed
855
856
    }
    else {
857
      return FALSE;
Dries's avatar
   
Dries committed
858
859
    }
  }
Dries's avatar
   
Dries committed
860
  else {
861
862
    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');
863
    return FALSE;
Dries's avatar
   
Dries committed
864
865
866
867
  }
}

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

Dries's avatar
   
Dries committed
870
  $links = array();
Dries's avatar
   
Dries committed
871

872
  // If we are viewing just this comment, we link back to the node.
Dries's avatar
   
Dries committed
873
  if ($return) {
874
    $links['comment_parent'] = array(
875
876
877
      'title' => t('parent'),
      'href' => comment_node_url(),
      'fragment' => "comment-$comment->cid"
878
    );
Dries's avatar
   
Dries committed
879
  }
Dries's avatar
   
Dries committed
880

881
  if (node_comment_mode($comment->nid) == COMMENT_NODE_READ_WRITE) {
882
    if (user_access('administer comments') && user_access('post comments')) {
883
      $links['comment_delete'] = array(
884
885
        'title' => t('delete'),
        'href' => "comment/delete/$comment->cid"
886
887
      );
      $links['comment_edit'] = array(
888
889
        'title' => t('edit'),
        'href' => "comment/edit/$comment->cid"
890
891
      );
      $links['comment_reply'] = array(
892
893
        'title' => t('reply'),
        'href' => "comment/reply/$comment->nid/$comment->cid"
894
      );
895
    }
896
897
    else if (user_access('post comments')) {
      if (comment_access('edit', $comment)) {
898
        $links['comment_edit'] = array(
899
900
          'title' => t('edit'),
          'href' => "comment/edit/$comment->cid"
901
        );
Dries's avatar
   
Dries committed
902
      }
903
      $links['comment_reply'] = array(
904
905
        'title' => t('reply'),
        'href' => "comment/reply/$comment->nid/$comment->cid"
906
      );
Dries's avatar
   
Dries committed
907
908
    }
    else {
909
910
      $node = node_load($comment->nid);
      $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node);
Dries's avatar
   
Dries committed
911
    }
Dries's avatar
   
Dries committed
912
  }
Dries's avatar
   
Dries committed
913

Dries's avatar
   
Dries committed
914
  return $links;
Dries's avatar
   
Dries committed
915
916
}

917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973