comment.module 70 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
/*
Dries's avatar
Dries committed
14
 * Constants to define a comment's published state
15
 */
Dries's avatar
Dries committed
16 17 18 19 20 21
define('COMMENT_PUBLISHED', 0);
define('COMMENT_NOT_PUBLISHED', 1);

/**
 * Constants to define the viewing modes for comment listings
 */
22 23 24 25
define('COMMENT_MODE_FLAT_COLLAPSED', 1);
define('COMMENT_MODE_FLAT_EXPANDED', 2);
define('COMMENT_MODE_THREADED_COLLAPSED', 3);
define('COMMENT_MODE_THREADED_EXPANDED', 4);
Dries's avatar
Dries committed
26 27 28 29

/**
 * Constants to define the viewing orders for comment listings
 */
30 31
define('COMMENT_ORDER_NEWEST_FIRST', 1);
define('COMMENT_ORDER_OLDEST_FIRST', 2);
Dries's avatar
Dries committed
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

/**
 * Constants to define the position of the comment controls
 */
define('COMMENT_CONTROLS_ABOVE', 0);
define('COMMENT_CONTROLS_BELOW', 1);
define('COMMENT_CONTROLS_ABOVE_BELOW', 2);
define('COMMENT_CONTROLS_HIDDEN', 3);

/**
 * Constants to define the anonymous poster contact handling
 */
define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0);
define('COMMENT_ANONYMOUS_MAY_CONTACT', 1);
define('COMMENT_ANONYMOUS_MUST_CONTACT', 2);

/**
 * Constants to define the comment form location
 */
define('COMMENT_FORM_SEPARATE_PAGE', 0);
define('COMMENT_FORM_BELOW', 1);

/**
 * Constants to define a node's comment state
 */
define('COMMENT_NODE_DISABLED', 0);
define('COMMENT_NODE_READ_ONLY', 1);
define('COMMENT_NODE_READ_WRITE', 2);
60

61 62 63 64 65 66
/**
 * Constants to define if comment preview is optional or required
 */
define('COMMENT_PREVIEW_OPTIONAL', 0);
define('COMMENT_PREVIEW_REQUIRED', 1);

67 68 69
/**
 * Implementation of hook_help().
 */
70
function comment_help($section) {
Dries's avatar
 
Dries committed
71
  switch ($section) {
Dries's avatar
 
Dries committed
72
    case 'admin/help#comment':
73
      $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>';
74
      $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>';
75
      $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>';
76
      return $output;
77 78
    case 'admin/content/comment':
    case 'admin/content/comment/new':
79
      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>';
80
    case 'admin/content/comment/approval':
81
      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>';
82
    case 'admin/content/comment/settings':
83
      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>';
84
   }
Dries's avatar
 
Dries committed
85 86
}

Dries's avatar
 
Dries committed
87 88 89
/**
 * Implementation of hook_menu().
 */
Dries's avatar
 
Dries committed
90
function comment_menu($may_cache) {
Dries's avatar
 
Dries committed
91 92
  $items = array();

Dries's avatar
 
Dries committed
93 94
  if ($may_cache) {
    $access = user_access('administer comments');
95 96
    $items[] = array(
      'path' => 'admin/content/comment',
97
      'title' => t('Comments'),
98
      'description' => t('List and edit site comments and the comment moderation queue.'),
99
      'callback' => 'comment_admin',
100 101
      'access' => $access
    );
Dries's avatar
 
Dries committed
102 103

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

    // Subtabs:
108
    $items[] = array('path' => 'admin/content/comment/list/new', 'title' => t('Published comments'),
Dries's avatar
 
Dries committed
109
      'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
110
    $items[] = array('path' => 'admin/content/comment/list/approval', 'title' => t('Approval queue'),
111 112
      'callback' => 'comment_admin',
      'callback arguments' => array('approval'),
113
      'access' => $access,
Dries's avatar
 
Dries committed
114 115
      'type' => MENU_LOCAL_TASK);

Dries's avatar
Dries committed
116
    $items[] = array(
117
      'path' => 'admin/content/comment/settings',
118
      'title' => t('Settings'),
119 120
      'callback' => 'drupal_get_form',
      'callback arguments' => array('comment_admin_settings'),
Dries's avatar
Dries committed
121
      'access' => $access,
122 123
      'weight' => 10,
      'type' => MENU_LOCAL_TASK);
124

125
    $items[] = array('path' => 'comment/delete', 'title' => t('Delete comment'),
126
      'callback' => 'comment_delete', 'access' => $access, 'type' => MENU_CALLBACK);
Dries's avatar
 
Dries committed
127 128

    $access = user_access('post comments');
129
    $items[] = array('path' => 'comment/edit', 'title' => t('Edit comment'),
130 131
      'callback' => 'comment_edit',
      'access' => $access, 'type' => MENU_CALLBACK);
Dries's avatar
 
Dries committed
132
  }
Dries's avatar
 
Dries committed
133 134
  else {
    if (arg(0) == 'comment' && arg(1) == 'reply' && is_numeric(arg(2))) {
135
      $node = node_load(arg(2));
Dries's avatar
 
Dries committed
136
      if ($node->nid) {
137
        $items[] = array('path' => 'comment/reply', 'title' => t('Reply to comment'),
Dries's avatar
 
Dries committed
138 139 140 141
          'callback' => 'comment_reply', 'access' => node_access('view', $node), 'type' => MENU_CALLBACK);
      }
    }
    if ((arg(0) == 'node') && is_numeric(arg(1)) && is_numeric(arg(2))) {
142 143 144 145 146 147 148
      $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
149
    }
Dries's avatar
 
Dries committed
150
  }
Dries's avatar
 
Dries committed
151 152 153 154 155 156 157 158

  return $items;
}

/**
 * Implementation of hook_perm().
 */
function comment_perm() {
159
  return array('access comments', 'post comments', 'administer comments', 'post comments without approval');
Dries's avatar
 
Dries committed
160 161 162 163 164 165 166 167 168 169 170 171
}

/**
 * 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;
  }
172
  else if ($op == 'view' && user_access('access comments')) {
Dries's avatar
 
Dries committed
173
    $block['subject'] = t('Recent comments');
174
    $block['content'] = theme('comment_block');
Dries's avatar
 
Dries committed
175 176 177 178
    return $block;
  }
}

179
function theme_comment_block() {
180
  $result = db_query_range(db_rewrite_sql('SELECT c.nid, c.subject, c.cid, c.timestamp FROM {comments} c INNER JOIN {node} n ON n.nid = c.nid WHERE n.status = 1 AND c.status = %d ORDER BY c.timestamp DESC', 'c'), COMMENT_PUBLISHED, 0, 10);
181 182
  $items = array();
  while ($comment = db_fetch_object($result)) {
183
    $items[] = l($comment->subject, 'node/'. $comment->nid, NULL, NULL, 'comment-'. $comment->cid) .'<br />'. t('@time ago', array('@time' => format_interval(time() - $comment->timestamp)));
184
  }
185 186 187
  if ($items) {
    return theme('item_list', $items);
  }
188 189
}

Dries's avatar
 
Dries committed
190 191 192
/**
 * Implementation of hook_link().
 */
193
function comment_link($type, $node = NULL, $teaser = FALSE) {
Dries's avatar
 
Dries committed
194 195 196 197
  $links = array();

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

198
    if ($teaser) {
Dries's avatar
 
Dries committed
199 200 201 202 203 204 205
      // Main page: display the number of comments that have been posted.

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

        if ($all) {
206
          $links['comment_comments'] = array(
207
            'title' => format_plural($all, '1 comment', '@count comments'),
208 209
            'href' => "node/$node->nid",
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
210
            'fragment' => 'comments'
211
          );
Dries's avatar
 
Dries committed
212 213

          if ($new) {
214
            $links['comment_new_comments'] = array(
215
              'title' => format_plural($new, '1 new comment', '@count new comments'),
216 217 218
              'href' => "node/$node->nid",
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
              'fragment' => 'new'
219
            );
Dries's avatar
 
Dries committed
220 221 222
          }
        }
        else {
Dries's avatar
Dries committed
223
          if ($node->comment == COMMENT_NODE_READ_WRITE) {
Dries's avatar
 
Dries committed
224
            if (user_access('post comments')) {
225
              $links['comment_add'] = array(
226
                'title' => t('Add new comment'),
227 228 229
                'href' => "comment/reply/$node->nid",
                'attributes' => array('title' => t('Add a new comment to this page.')),
                'fragment' => 'comment_form'
230
              );
Dries's avatar
 
Dries committed
231 232
            }
            else {
233
              $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node->nid);
Dries's avatar
 
Dries committed
234 235 236 237 238 239 240 241 242
            }
          }
        }
      }
    }
    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

243
      if ($node->comment == COMMENT_NODE_READ_WRITE) {
Dries's avatar
 
Dries committed
244
        if (user_access('post comments')) {
245
          if (variable_get('comment_form_location', COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_SEPARATE_PAGE) {
246
            $links['comment_add'] = array(
247
              'title' => t('Add new comment'),
248 249 250
              'href' => "comment/reply/$node->nid",
              'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
              'fragment' => 'comment_form'
251
            );
252
          }
Dries's avatar
 
Dries committed
253 254
        }
        else {
255
          $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node->nid);
Dries's avatar
 
Dries committed
256 257 258 259 260 261
        }
      }
    }
  }

  if ($type == 'comment') {
262
    $links = comment_links($node, $teaser);
Dries's avatar
 
Dries committed
263
  }
264 265 266
  if (isset($links['comment_forbidden'])) {
    $links['comment_forbidden']['html'] = TRUE;
  }
Dries's avatar
 
Dries committed
267 268 269 270

  return $links;
}

271
function comment_form_alter($form_id, &$form) {
272 273 274 275
  if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
    $form['workflow']['comment'] = array(
      '#type' => 'radios',
      '#title' => t('Default comment setting'),
276
      '#default_value' => variable_get('comment_'. $form['#node_type']->type, COMMENT_NODE_READ_WRITE),
277 278 279 280 281
      '#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'])) {
282
    if ($form['type']['#value'] .'_node_form' == $form_id) {
283
      $node = $form['#node'];
284 285 286 287 288 289 290 291 292 293 294 295 296 297
      $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')),
      );
298
    }
299 300 301
  }
}

Dries's avatar
 
Dries committed
302 303
/**
 * Implementation of hook_nodeapi().
Dries's avatar
 
Dries committed
304
 *
Dries's avatar
 
Dries committed
305 306 307
 */
function comment_nodeapi(&$node, $op, $arg = 0) {
  switch ($op) {
Dries's avatar
 
Dries committed
308
    case 'load':
309
      return db_fetch_array(db_query("SELECT last_comment_timestamp, last_comment_name, comment_count FROM {node_comment_statistics} WHERE nid = %d", $node->nid));
310 311 312 313
      break;

    case 'prepare':
      if (!isset($node->comment)) {
Dries's avatar
Dries committed
314
        $node->comment = variable_get("comment_$node->type", COMMENT_NODE_READ_WRITE);
Dries's avatar
 
Dries committed
315 316
      }
      break;
317

318 319 320 321 322 323 324 325 326
    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
327 328
    case 'update index':
      $text = '';
329
      $comments = db_query('SELECT subject, comment, format FROM {comments} WHERE nid = %d AND status = %d', $node->nid, COMMENT_PUBLISHED);
Dries's avatar
Dries committed
330
      while ($comment = db_fetch_object($comments)) {
331
        $text .= '<h2>'. check_plain($comment->subject) .'</h2>'. check_markup($comment->comment, $comment->format, FALSE);
Dries's avatar
Dries committed
332 333
      }
      return $text;
334

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

Steven Wittens's avatar
- Typo  
Steven Wittens committed
339
    case 'rss item':
340
      if ($node->comment != COMMENT_NODE_DISABLED) {
341
        return array(array('key' => 'comments', 'value' => url('node/'. $node->nid, NULL, 'comments', TRUE)));
342 343 344 345
      }
      else {
        return array();
      }
Dries's avatar
 
Dries committed
346 347 348 349 350 351 352 353 354 355 356
  }
}

/**
 * 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
357 358 359 360 361 362 363 364 365 366
    $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.'));
367 368

    return $form;
Dries's avatar
 
Dries committed
369
  }
370 371 372 373
  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
374 375
}

376
/**
Dries's avatar
 
Dries committed
377
 * Menu callback; presents the comment settings page.
378
 */
Dries's avatar
Dries committed
379
function comment_admin_settings() {
380 381
  $form['viewing_options'] = array(
    '#type' => 'fieldset',
382
    '#title' => t('Viewing options'),
383 384
    '#collapsible' => TRUE,
  );
Dries's avatar
 
Dries committed
385

Dries's avatar
Dries committed
386 387 388 389 390
  $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(),
391
    '#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
392
  );
393

Dries's avatar
Dries committed
394 395 396
  $form['viewing_options']['comment_default_order'] = array(
    '#type' => 'radios',
    '#title' => t('Default display order'),
397
    '#default_value' => variable_get('comment_default_order', COMMENT_ORDER_NEWEST_FIRST),
Dries's avatar
Dries committed
398
    '#options' => _comment_get_orders(),
399
    '#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
400
  );
Dries's avatar
 
Dries committed
401

402
  $form['viewing_options']['comment_default_per_page'] = array(
403 404 405 406
    '#type' => 'select',
    '#title' => t('Default comments per page'),
    '#default_value' => variable_get('comment_default_per_page', 50),
    '#options' => _comment_per_page(),
407
    '#description' => t('Default number of comments for each page: more comments are distributed in several pages.'),
408 409
  );

Dries's avatar
Dries committed
410 411 412 413 414 415 416 417 418
  $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')),
419
    '#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
420
  );
421

422 423
  $form['posting_settings'] = array(
    '#type' => 'fieldset',
424
    '#title' => t('Posting settings'),
425 426
    '#collapsible' => TRUE,
  );
427

Dries's avatar
Dries committed
428 429
  $form['posting_settings']['comment_anonymous'] = array(
    '#type' => 'radios',
430
    '#title' => t('Anonymous commenting'),
Dries's avatar
Dries committed
431 432 433 434 435
    '#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')),
436
    '#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
437
  );
438
  if (!user_access('post comments', user_load(array('uid' => 0)))) {
439
    $form['posting_settings']['comment_anonymous']['#disabled'] = TRUE;
440
  }
441 442

  $form['posting_settings']['comment_subject_field'] = array(
443 444 445 446
    '#type' => 'radios',
    '#title' => t('Comment subject field'),
    '#default_value' => variable_get('comment_subject_field', 1),
    '#options' => array(t('Disabled'), t('Enabled')),
447
    '#description' => t('Can users provide a unique subject for their comments?'),
448 449
  );

450 451 452 453 454 455
  $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')),
  );
456

Dries's avatar
Dries committed
457 458 459 460
  $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),
461
    '#options' => array(t('Display on separate page'), t('Display below post or comments')),
Dries's avatar
Dries committed
462
  );
463

464
  return system_settings_form($form);
Dries's avatar
 
Dries committed
465 466
}

467 468 469 470 471 472
/**
 * 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
473
 * statements based on the replies to their posts.
474
 */
Dries's avatar
 
Dries committed
475
function comment_access($op, $comment) {
Dries's avatar
 
Dries committed
476 477
  global $user;

478
  if ($op == 'edit') {
479
    return ($user->uid && $user->uid == $comment->uid && comment_num_replies($comment->cid) == 0) || user_access('administer comments');
Dries's avatar
 
Dries committed
480 481
  }
}
482

Dries's avatar
 
Dries committed
483
function comment_node_url() {
Dries's avatar
Dries committed
484
  return arg(0) .'/'. arg(1);
Dries's avatar
 
Dries committed
485
}
Dries's avatar
 
Dries committed
486

Dries's avatar
 
Dries committed
487 488 489
function comment_edit($cid) {
  global $user;

490
  $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
491
  $comment = drupal_unpack($comment);
492
  $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
493
  if (comment_access('edit', $comment)) {
494
    return comment_form_box((array)$comment);
495 496 497
  }
  else {
    drupal_access_denied();
Dries's avatar
 
Dries committed
498 499 500
  }
}

501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
/**
 * 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
521
function comment_reply($nid, $pid = NULL) {
522
  // Load the parent node.
523
  $node = node_load($nid);
524 525

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

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

530
  $output = '';
Dries's avatar
 
Dries committed
531

Dries's avatar
Dries committed
532
  if (user_access('access comments')) {
533
    // The user is previewing a comment prior to submitting it.
534 535
    if ($op == t('Preview comment')) {
      if (user_access('post comments')) {
536
        $output .= comment_form_box(array('pid' => $pid, 'nid' => $nid), NULL);
537 538 539
      }
      else {
        drupal_set_message(t('You are not authorized to post comments.'), 'error');
540
        drupal_goto("node/$nid");
541
      }
Dries's avatar
 
Dries committed
542 543
    }
    else {
544
      // $pid indicates that this is a reply to a comment.
545
      if ($pid) {
546
        // load the comment whose cid = $pid
547
        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))) {
548 549
          // If that comment exists, make sure that the current comment and the parent comment both
          // belong to the same parent node.
550 551 552 553 554
          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");
          }
555
          // Display the parent comment
556 557 558 559 560 561 562 563
          $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");
        }
564
      }
565
      // This is the case where the comment is in response to a node. Display the node.
566 567 568 569
      else if (user_access('access content')) {
        $output .= node_view($node);
      }

570
      // Should we show the reply box?
571 572
      if (node_comment_mode($nid) != COMMENT_NODE_READ_WRITE) {
        drupal_set_message(t("This discussion is closed: you can't post new comments."), 'error');
573
        drupal_goto("node/$nid");
574 575
      }
      else if (user_access('post comments')) {
576
        $output .= comment_form_box(array('pid' => $pid, 'nid' => $nid), t('Reply'));
577 578 579
      }
      else {
        drupal_set_message(t('You are not authorized to post comments.'), 'error');
580
        drupal_goto("node/$nid");
581
      }
Dries's avatar
 
Dries committed
582
    }
Kjartan's avatar
Kjartan committed
583 584
  }
  else {
585
    drupal_set_message(t('You are not authorized to view comments.'), 'error');
586
    drupal_goto("node/$nid");
Dries's avatar
 
Dries committed
587
  }
Dries's avatar
 
Dries committed
588

Dries's avatar
 
Dries committed
589
  return $output;
Dries's avatar
 
Dries committed
590 591
}

592 593 594 595 596 597 598
/**
 * Accepts a submission of new or changed comment content.
 *
 * @param $edit
 *   A comment array.
 *
 * @return
599
 *   If the comment is successfully saved the comment ID is returned. If the comment
600 601 602
 *   is not saved, FALSE is returned.
 */
function comment_save($edit) {
Dries's avatar
 
Dries committed
603
  global $user;
604
  if (user_access('post comments') && (user_access('administer comments') || node_comment_mode($edit['nid']) == COMMENT_NODE_READ_WRITE)) {
Dries's avatar
 
Dries committed
605
    if (!form_get_errors()) {
606
      // Check for duplicate comments. Note that we have to use the
607
      // validated/filtered data to perform such check.
608
      $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);
Dries's avatar
 
Dries committed
609
      if ($duplicate != 0) {
610
        watchdog('content', t('Comment: duplicate %subject.', array('%subject' => $edit['subject'])), WATCHDOG_WARNING);
Dries's avatar
 
Dries committed
611
      }
Dries's avatar
 
Dries committed
612

613
      if ($edit['cid']) {
614
        // Update the comment in the database.
615
        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
616

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

619
        // Allow modules to respond to the updating of a comment.
620 621
        comment_invoke_comment($edit, 'update');

Dries's avatar
 
Dries committed
622

Dries's avatar
Dries committed
623
        // Add an entry to the watchdog log.
624
        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
625 626
      }
      else {
627
        // Add the comment to database.
628
        $status = user_access('post comments without approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
629
        $roles = variable_get('comment_roles', array());
Dries's avatar
 
Dries committed
630 631 632 633 634 635
        $score = 0;

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

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

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

645 646
          // Strip the "/" from the end of the thread.
          $max = rtrim($max, '/');
Dries's avatar
 
Dries committed
647

648
          // Finally, build the thread field for this new comment.
649
          $thread = int2vancode(vancode2int($max) + 1) .'/';
Dries's avatar
 
Dries committed
650 651
        }
        else {
652 653
          // This is comment with a parent comment: we increase
          // the part of the thread value at the proper depth.
Dries's avatar
 
Dries committed
654 655

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

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

661
          // Get the max value in _this_ thread.
Dries's avatar
 
Dries committed
662
          $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
663

664 665
          if ($max == '') {
            // First child of this parent.
666
            $thread = $parent->thread .'.'. int2vancode(0) .'/';
Dries's avatar
 
Dries committed
667 668
          }
          else {
669 670
            // Strip the "/" at the end of the thread.
            $max = rtrim($max, '/');
Dries's avatar
 
Dries committed
671

672 673 674
            // We need to get the value at the correct depth.
            $parts = explode('.', $max);
            $parent_depth = count(explode('.', $parent->thread));
Dries's avatar
 
Dries committed
675 676
            $last = $parts[$parent_depth];

677
            // Finally, build the thread field for this new comment.
678
            $thread = $parent->thread .'.'. int2vancode(vancode2int($last) + 1) .'/';
Dries's avatar
 
Dries committed
679 680 681
          }
        }

682
        $edit['cid'] = db_next_id('{comments}_cid');
Dries's avatar
 
Dries committed
683 684
        $edit['timestamp'] = time();

685
        if ($edit['uid'] === $user->uid) { // '===' because we want to modify anonymous users too
Dries's avatar
 
Dries committed
686 687 688
          $edit['name'] = $user->name;
        }

689
        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
690 691

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

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

696
        // Add an entry to the watchdog log.
697
        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
698
      }
Dries's avatar
 
Dries committed
699

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

Dries's avatar
 
Dries committed
703
      // Explain the approval queue if necessary, and then
Dries's avatar
 
Dries committed
704
      // redirect the user to the node he's commenting on.
705
      if ($status == COMMENT_NOT_PUBLISHED) {
Dries's avatar
 
Dries committed
706
        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
707
      }
708
      return $edit['cid'];
Dries's avatar
 
Dries committed
709 710
    }
    else {
711
      return FALSE;
Dries's avatar
 
Dries committed
712 713
    }
  }
Dries's avatar
 
Dries committed
714
  else {
715
    $txt = t('Comment: unauthorized comment submitted or comment submitted to a closed node %subject.', array('%subject' => $edit['subject']));
716 717 718
    watchdog('content', $txt, WATCHDOG_WARNING);
    drupal_set_message($txt, 'error');
    return FALSE;
Dries's avatar
 
Dries committed
719 720 721 722
  }
}

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

Dries's avatar
 
Dries committed
725
  $links = array();
Dries's avatar
 
Dries committed
726

727
  // If we are viewing just this comment, we link back to the node.
Dries's avatar
 
Dries committed
728
  if ($return) {
729
    $links['comment_parent'] = array(
730 731 732
      'title' => t('parent'),
      'href' => comment_node_url(),
      'fragment' => "comment-$comment->cid"
733
    );
Dries's avatar
 
Dries committed
734
  }
Dries's avatar
 
Dries committed
735

736
  if (node_comment_mode($comment->nid) == COMMENT_NODE_READ_WRITE) {
737
    if (user_access('administer comments') && user_access('post comments')) {
738
      $links['comment_delete'] = array(
739 740
        'title' => t('delete'),
        'href' => "comment/delete/$comment->cid"
741 742
      );
      $links['comment_edit'] = array(
743 744
        'title' => t('edit'),
        'href' => "comment/edit/$comment->cid"
745 746
      );
      $links['comment_reply'] = array(
747 748
        'title' => t('reply'),
        'href' => "comment/reply/$comment->nid/$comment->cid"
749
      );
750
    }
751 752
    else if (user_access('post comments')) {
      if (comment_access('edit', $comment)) {
753
        $links['comment_edit'] = array(
754 755
          'title' => t('edit'),
          'href' => "comment/edit/$comment->cid"
756
        );
Dries's avatar
 
Dries committed
757
      }
758
      $links['comment_reply'] = array(
759 760
        'title' => t('reply'),
        'href' => "comment/reply/$comment->nid/$comment->cid"
761
      );
Dries's avatar
 
Dries committed
762 763
    }
    else {
764
      $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $comment->nid);
Dries's avatar
 
Dries committed
765
    }
Dries's avatar
 
Dries committed
766
  }
Dries's avatar
 
Dries committed
767

Dries's avatar
 
Dries committed
768
  return $links;
Dries's avatar
 
Dries committed
769 770
}

771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832
/**
 * 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