comment.module 78.4 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 awaiting approval.
15
 */
16
define('COMMENT_NOT_PUBLISHED', 0);
17 18

/**
19
 * Comment is published.
20
 */
21
define('COMMENT_PUBLISHED', 1);
Dries's avatar
Dries committed
22

23 24 25
/**
 * Comments are displayed in a flat list - expanded.
 */
26
define('COMMENT_MODE_FLAT', 0);
27 28 29 30

/**
 * Comments are displayed as a threaded list - expanded.
 */
31
define('COMMENT_MODE_THREADED', 1);
Dries's avatar
Dries committed
32 33

/**
34
 * Anonymous posters cannot enter their contact information.
Dries's avatar
Dries committed
35 36
 */
define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0);
37 38 39 40

/**
 * Anonymous posters may leave their contact information.
 */
Dries's avatar
Dries committed
41
define('COMMENT_ANONYMOUS_MAY_CONTACT', 1);
42 43

/**
44
 * Anonymous posters are required to leave their contact information.
45
 */
Dries's avatar
Dries committed
46 47 48
define('COMMENT_ANONYMOUS_MUST_CONTACT', 2);

/**
49
 * Comment form should be displayed on a separate page.
Dries's avatar
Dries committed
50 51
 */
define('COMMENT_FORM_SEPARATE_PAGE', 0);
52 53 54 55

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

/**
59
 * Comments for this node are hidden.
Dries's avatar
Dries committed
60
 */
61
define('COMMENT_NODE_HIDDEN', 0);
62 63

/**
64
 * Comments for this node are closed.
65
 */
66
define('COMMENT_NODE_CLOSED', 1);
67 68

/**
69
 * Comments for this node are open.
70
 */
71
define('COMMENT_NODE_OPEN', 2);
72

73
/**
74
 * Comment preview is optional.
75 76
 */
define('COMMENT_PREVIEW_OPTIONAL', 0);
77 78 79 80

/**
 * Comment preview is required.
 */
81 82
define('COMMENT_PREVIEW_REQUIRED', 1);

83
/**
84
 * Implement hook_help().
85
 */
86 87
function comment_help($path, $arg) {
  switch ($path) {
Dries's avatar
 
Dries committed
88
    case 'admin/help#comment':
89
      $output  = '<p>' . t('The comment module allows visitors to comment on your posts, creating ad hoc discussion boards. Any <a href="@content-type">content type</a> may have its <em>Default comment setting</em> set to <em>Open</em> to allow comments, <em>Hidden</em> to hide existing comments and prevent new comments or <em>Closed</em> to allow existing comments to be viewed but no new comments added. Comment display settings and other controls may also be customized for each content type.', array('@content-type' => url('admin/structure/types'))) . '</p>';
90
      $output .= '<p>' . t('Comment permissions are assigned to user roles, and are used to determine whether anonymous users (or other roles) are allowed to comment on posts. If anonymous users are allowed to comment, their individual contact information may be retained in cookies stored on their local computer for use in later comment submissions. When a comment has no replies, it may be (optionally) edited by its author. The comment module uses the same text formats and HTML tags available when creating other forms of content.') . '</p>';
91
      $output .= '<p>' . t('Change comment settings on the content type\'s <a href="@content-type">edit page</a>.', array('@content-type' => url('admin/structure/types'))) . '</p>';
92
      $output .= '<p>' . t('For more information, see the online handbook entry for <a href="@comment">Comment module</a>.', array('@comment' => 'http://drupal.org/handbook/modules/comment/')) . '</p>';
93

94
      return $output;
95
  }
Dries's avatar
 
Dries committed
96 97
}

98
/**
99
 * Implement hook_theme().
100 101 102 103 104 105 106
 */
function comment_theme() {
  return array(
    'comment_block' => array(
      'arguments' => array(),
    ),
    'comment_preview' => array(
107
      'arguments' => array('comment' => NULL),
108 109
    ),
    'comment' => array(
110
      'template' => 'comment',
111
      'arguments' => array('elements' => NULL),
112 113 114 115 116
    ),
    'comment_post_forbidden' => array(
      'arguments' => array('nid' => NULL),
    ),
    'comment_wrapper' => array(
117
      'template' => 'comment-wrapper',
118
      'arguments' => array('content' => NULL),
119
    ),
120 121 122
    'comment_submitted' => array(
      'arguments' => array('comment' => NULL),
    ),
123 124 125
  );
}

Dries's avatar
 
Dries committed
126
/**
127
 * Implement hook_menu().
Dries's avatar
 
Dries committed
128
 */
129
function comment_menu() {
130
  $items['admin/content/comment'] = array(
131
    'title' => 'Comments',
132
    'description' => 'List and edit site comments and the comment approval queue.',
133 134
    'page callback' => 'comment_admin',
    'access arguments' => array('administer comments'),
135
    'type' => MENU_LOCAL_TASK,
136
  );
137
  // Tabs begin here.
138
  $items['admin/content/comment/new'] = array(
139
    'title' => 'Published comments',
140 141 142
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
143
  $items['admin/content/comment/approval'] = array(
144
    'title' => 'Approval queue',
145
    'page arguments' => array('approval'),
146
    'access arguments' => array('administer comments'),
147 148 149
    'type' => MENU_LOCAL_TASK,
  );
  $items['comment/delete'] = array(
150
    'title' => 'Delete comment',
151
    'page callback' => 'comment_delete_page',
152 153 154
    'access arguments' => array('administer comments'),
    'type' => MENU_CALLBACK,
  );
155
  $items['comment/edit/%comment'] = array(
156
    'title' => 'Edit comment',
157 158
    'page callback' => 'drupal_get_form',
    'page arguments' => array('comment_form', 2),
159 160
    'access callback' => 'comment_access',
    'access arguments' => array('edit', 2),
161 162
    'type' => MENU_CALLBACK,
  );
163
  $items['comment/reply/%node'] = array(
164
    'title' => 'Add new comment',
165
    'page callback' => 'comment_reply',
166
    'page arguments' => array(2),
167 168 169 170
    'access callback' => 'node_access',
    'access arguments' => array('view', 2),
    'type' => MENU_CALLBACK,
  );
171
  $items['comment/approve'] = array(
172
    'title' => 'Approve a comment',
173 174 175 176 177
    'page callback' => 'comment_approve',
    'page arguments' => array(2),
    'access arguments' => array('administer comments'),
    'type' => MENU_CALLBACK,
  );
178 179 180 181 182 183 184
  $items['comment/%comment'] = array(
    'title' => 'Comment permalink',
    'page callback' => 'comment_permalink',
    'page arguments' => array(1),
    'access arguments' => array('access comments'),
    'type' => MENU_CALLBACK,
  );
Dries's avatar
 
Dries committed
185 186 187 188

  return $items;
}

189
/**
190
 * Implement hook_fieldable_info().
191
 */
192 193 194 195 196 197 198 199 200 201 202 203 204
function comment_fieldable_info() {
  $return = array(
    'comment' => array(
      'label' => t('Comment'),
      'object keys' => array(
        'id' => 'cid',
        'bundle' => 'node_type',
      ),
      'bundle keys' => array(
        'bundle' => 'type',
      ),
      'bundles' => array(),
    ),
205
  );
206 207 208 209 210 211 212
  foreach (node_type_get_names() as $type => $name) {
    $return['comment']['bundles']['comment_node_' . $type] = array(
      'label' => $name,
    );
  }
  return $return;
}
213

214 215 216 217 218

/**
 * Implement hook_node_type().
 */
function comment_node_type($op, $info) {
219
  switch ($op) {
220 221 222 223 224 225 226 227 228 229
    case 'insert':
      field_attach_create_bundle('comment_node_' . $info->type);
      break;

    case 'update':
      if (!empty($info->old_type) && $info->type != $info->old_type) {
        field_attach_rename_bundle('comment_node_' . $info->old_type, 'comment_node_' . $info->type);
      }
      break;

230
    case 'delete':
231 232 233 234 235 236 237 238 239 240 241
      field_attach_delete_bundle('comment_node_' . $info->type);

      $settings = array(
        'comment',
        'comment_default_mode',
        'comment_default_per_page',
        'comment_anonymous',
        'comment_subject_field',
        'comment_preview',
        'comment_form_location',
      );
242
      foreach ($settings as $setting) {
243
        variable_del($setting . '_' . $info->type);
244 245 246 247 248
      }
      break;
  }
}

Dries's avatar
 
Dries committed
249
/**
250
 * Implement hook_permission().
Dries's avatar
 
Dries committed
251
 */
252
function comment_permission() {
253
  return array(
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
    'administer comments' => array(
      'title' => t('Administer comments'),
      'description' => t('Manage and approve comments, and configure comment administration settings.'),
    ),
    'access comments' => array(
      'title' => t('Access comments'),
      'description' => t('View comments attached to content.'),
    ),
    'post comments' => array(
      'title' => t('Post comments'),
      'description' => t('Add comments to content (approval required).'),
    ),
    'post comments without approval' => array(
      'title' => t('Post comments without approval'),
      'description' => t('Add comments to content (no approval required).'),
    ),
270
  );
Dries's avatar
 
Dries committed
271 272 273
}

/**
274
 * Implement hook_block_list().
Dries's avatar
 
Dries committed
275
 */
276 277
function comment_block_list() {
  $blocks['recent']['info'] = t('Recent comments');
278

279 280
  return $blocks;
}
281

282
/**
283
 * Implement hook_block_configure().
284 285 286 287 288 289 290 291
 */
function comment_block_configure($delta = '') {
  $form['comment_block_count'] = array(
    '#type' => 'select',
    '#title' => t('Number of recent comments'),
    '#default_value' => variable_get('comment_block_count', 10),
    '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30)),
  );
292

293 294
  return $form;
}
295

296
/**
297
 * Implement hook_block_save().
298 299 300 301
 */
function comment_block_save($delta = '', $edit = array()) {
  variable_set('comment_block_count', (int)$edit['comment_block_count']);
}
302

303
/**
304
 * Implement hook_block_view().
305 306 307 308 309 310 311
 *
 * Generates a block with the most recent comments.
 */
function comment_block_view($delta = '') {
  if (user_access('access comments')) {
    $block['subject'] = t('Recent comments');
    $block['content'] = theme('comment_block');
312

313
    return $block;
Dries's avatar
 
Dries committed
314 315 316
  }
}

317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
/**
 * Redirects comment links to the correct page depending on comment settings.
 *
 * Since comments are paged there is no way to guarantee which page a comment
 * appears on. Comment paging and threading settings may be changed at any time.
 * With threaded comments, an individual comment may move between pages as
 * comments can be added either before or after it in the overall discussion.
 * Therefore we use a central routing function for comment links, which
 * calculates the page number based on current comment settings and returns
 * the full comment view with the pager set dynamically.
 *
 * @param $comment
 *   A comment object.
 * @return
 *   The comment listing set to the page on which the comment appears.
 */
function comment_permalink($comment) {
  $node = node_load($comment->nid);
  if ($node && $comment) {

    // Find the current display page for this comment.
    $page = comment_get_display_page($comment->cid, $node->type);

    // Set $_GET['q'] and $_GET['page'] ourselves so that the node callback
    // behaves as it would when visiting the page directly.
    $_GET['q'] = 'node/' . $node->nid;
    $_GET['page'] = $page;

    // Set the node path as the canonical URL to prevent duplicate content.
    drupal_add_link(array('rel' => 'canonical', 'href' => url('node/' . $node->nid)));

    // Return the node view, this will show the correct comment in context.
    return menu_execute_active_handler('node/' . $node->nid);
  }
  drupal_not_found();
}

354
/**
355 356 357 358 359 360 361
 * Find the most recent comments that are available to the current user.
 *
 * This is done in two steps:
 *   1. Query the {node_comment_statistics} table to find n number of nodes that
 *      have the most recent comments. This table is indexed on
 *      last_comment_timestamp, thus making it a fast query.
 *   2. Load the information from the comments table based on the nids found
362 363
 *      in step 1.
 *
364
 * @param integer $number
365 366 367
 *   (optional) The maximum number of comments to find.
 * @return
 *   An array of comment objects each containing a nid,
Dries's avatar
Dries committed
368
 *   subject, cid, and timestamp, or an empty array if there are no recent
369 370 371
 *   comments visible to the current user.
 */
function comment_get_recent($number = 10) {
372 373
  // Step 1: Select a $number of nodes which have new comments,
  //         and are visible to the current user.
374
  $nids = db_query_range("SELECT nc.nid FROM {node_comment_statistics} nc WHERE nc.comment_count > 0 ORDER BY nc.last_comment_timestamp DESC", 0, $number)->fetchCol();
375 376 377

  $comments = array();
  if (!empty($nids)) {
378 379
    // Step 2: From among the comments on the nodes selected in the first query,
    //         find the $number of most recent comments.
380
    // Using Query Builder here for the IN-Statement.
381 382
    $query = db_select('comment', 'c');
    $query->innerJoin('node', 'n', 'n.nid = c.nid');
383 384 385 386 387 388 389 390 391
    return $query
      ->fields('c', array('nid', 'subject', 'cid', 'timestamp'))
      ->condition('c.nid', $nids, 'IN')
      ->condition('c.status', COMMENT_PUBLISHED)
      ->condition('n.status', 1)
      ->orderBy('c.cid', 'DESC')
      ->range(0, $number)
      ->execute()
      ->fetchAll();
392 393 394 395 396
  }

  return $comments;
}

397 398
/**
 * Calculate page number for first new comment.
399 400 401 402 403 404 405 406 407
 *
 * @param $num_comments
 *   Number of comments.
 * @param $new_replies
 *   Number of new replies.
 * @param $node
 *   The first new comment node.
 * @return
 *   "page=X" if the page number is greater than zero; empty string otherwise.
408
 */
409 410 411
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);
412
  $pagenum = NULL;
413
  $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
414 415
  if ($num_comments <= $comments_per_page) {
    // Only one page of comments.
416
    $pageno = 0;
417
  }
418 419 420 421 422
  elseif ($flat) {
    // Flat comments.
    $count = $num_comments - $new_replies;
    $pageno =  $count / $comments_per_page;
  }
423
  else {
424 425
    // Threaded comments.
    // Find the first thread with a new comment.
426
    $result = db_query_range('SELECT thread FROM (SELECT thread
427
      FROM {comment}
428 429
      WHERE nid = :nid
        AND status = 0
430
      ORDER BY timestamp DESC) AS thread
431
      ORDER BY SUBSTRING(thread, 1, (LENGTH(thread) - 1))', array(':nid' => $node->nid), 0, $new_replies)->fetchField();
432
    $thread = substr($result, 0, -1);
433
    $count = db_query('SELECT COUNT(*) FROM {comment} WHERE nid = :nid AND status = 0 AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread', array(
434
      ':nid' => $node->nid,
435 436
      ':thread' => $thread,
    ))->fetchField();
437 438
    $pageno =  $count / $comments_per_page;
  }
439

440
  if ($pageno >= 1) {
441
    $pagenum = "page=" . intval($pageno);
442
  }
443

444 445 446
  return $pagenum;
}

447
/**
448
 * Returns a formatted list of recent comments to be displayed in the comment block.
449
 *
450 451
 * @return
 *   The comment list HTML.
452 453
 * @ingroup themeable
 */
454 455
function theme_comment_block() {
  $items = array();
456 457
  $number = variable_get('comment_block_count', 10);
  foreach (comment_get_recent($number) as $comment) {
458
    $items[] = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)) . '<br />' . t('@time ago', array('@time' => format_interval(REQUEST_TIME - $comment->timestamp)));
459
  }
460

461 462 463
  if ($items) {
    return theme('item_list', $items);
  }
464 465
}

Dries's avatar
 
Dries committed
466
/**
467
 * Implement hook_node_view().
Dries's avatar
 
Dries committed
468
 */
469
function comment_node_view($node, $build_mode) {
Dries's avatar
 
Dries committed
470 471
  $links = array();

472
  if ($node->comment) {
473
    if ($build_mode == 'rss') {
474 475 476 477 478 479 480
      if ($node->comment != COMMENT_NODE_HIDDEN) {
        // Add a comments RSS element which is a URL to the comments of this node.
        $node->rss_elements[] = array(
          'key' => 'comments',
          'value' => url('node/' . $node->nid, array('fragment' => 'comments', 'absolute' => TRUE))
        );
      }
481
    }
482
    elseif ($build_mode == 'teaser') {
Dries's avatar
 
Dries committed
483 484
      // Main page: display the number of comments that have been posted.
      if (user_access('access comments')) {
485
        if (!empty($node->comment_count)) {
486
          $links['comment_comments'] = array(
487
            'title' => format_plural($node->comment_count, '1 comment', '@count comments'),
488 489
            'href' => "node/$node->nid",
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
490 491
            'fragment' => 'comments',
            'html' => TRUE,
492
          );
493 494

          $new = comment_num_new($node->nid);
Dries's avatar
 
Dries committed
495
          if ($new) {
496
            $links['comment_new_comments'] = array(
497
              'title' => format_plural($new, '1 new comment', '@count new comments'),
498
              'href' => "node/$node->nid",
499
              'query' => comment_new_page_count($node->comment_count, $new, $node),
500
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
501 502
              'fragment' => 'new',
              'html' => TRUE,
503
            );
Dries's avatar
 
Dries committed
504 505 506
          }
        }
        else {
507
          if ($node->comment == COMMENT_NODE_OPEN) {
Dries's avatar
 
Dries committed
508
            if (user_access('post comments')) {
509
              $links['comment_add'] = array(
510
                'title' => t('Add new comment'),
511 512
                'href' => "comment/reply/$node->nid",
                'attributes' => array('title' => t('Add a new comment to this page.')),
513 514
                'fragment' => 'comment-form',
                'html' => TRUE,
515
              );
Dries's avatar
 
Dries committed
516 517
            }
            else {
518
              $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node);
Dries's avatar
 
Dries committed
519 520 521 522 523 524
            }
          }
        }
      }
    }
    else {
525 526
      // Node page: add a "post comment" link if the user is allowed to post
      // comments and if this node is not read-only.
527
      if ($node->comment == COMMENT_NODE_OPEN) {
Dries's avatar
 
Dries committed
528
        if (user_access('post comments')) {
529 530 531 532 533 534
          $links['comment_add'] = array(
            'title' => t('Add new comment'),
            'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
            'fragment' => 'comment-form',
            'html' => TRUE,
          );
535
          if (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_SEPARATE_PAGE) {
536 537 538 539
            $links['comment_add']['href'] = "comment/reply/$node->nid";
          }
          else {
            $links['comment_add']['href'] = "node/$node->nid";
540
          }
Dries's avatar
 
Dries committed
541 542
        }
        else {
543
          $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node);
Dries's avatar
 
Dries committed
544 545 546
        }
      }
    }
Dries's avatar
Dries committed
547

548 549 550
    if (isset($links['comment_forbidden'])) {
      $links['comment_forbidden']['html'] = TRUE;
    }
551

552
    $node->content['links']['comment'] = array(
553 554 555
      '#theme' => 'links',
      '#links' => $links,
      '#attributes' => array('class' => 'links inline'),
556
    );
Dries's avatar
Dries committed
557

558 559 560 561 562 563 564
    // Only append comments when we are building a node on its own node detail
    // page. We compare $node and $page_node to ensure that comments are not
    // appended to other nodes shown on the page, for example a node_reference
    // displayed in 'full' build mode within another node.
    $page_node = menu_get_object();
    if ($node->comment && isset($page_node->nid) && $page_node->nid == $node->nid && empty($node->in_preview) && user_access('access comments')) {
      $node->content['comments'] = comment_node_page_additions($node);
565
    }
566
  }
Dries's avatar
 
Dries committed
567 568
}

569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593
/**
 * Build the comment-related elements for node detail pages.
 *
 * @param $node
 *  A node object.
 */
function comment_node_page_additions($node) {
  $additions = array();

  // Only attempt to render comments if the node has visible comments.
  // Unpublished comments are not included in $node->comment_count, so show
  // comments unconditionally if the user is an administrator.
  if ($node->comment_count || user_access('administer comments')) {
    if ($cids = comment_get_thread($node)) {
      $comments = comment_load_multiple($cids);
      comment_prepare_thread($comments);
      $build = comment_build_multiple($comments);
      $build['#attached_css'][] = drupal_get_path('module', 'comment') . '/comment.css';
      $build['pager']['#theme'] = 'pager';
      $additions['comments'] = $build;
    }
  }

  // Append comment form if needed.
  if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) {
594
    $build = drupal_get_form('comment_form', (object) array('nid' => $node->nid));
595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 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
    $additions['comment_form'] = $build;
  }

  if ($additions) {
    $additions += array(
      '#theme' => 'comment_wrapper',
      '#node' => $node,
      'comments' => array(),
      'comment_form' => array(),
    );
  }

  return $additions;
}

/**
 * Retrieve comment(s) for a thread.
 *
 * @param $node
 *   The node whose comment(s) needs rendering.
 *
 * To display threaded comments in the correct order we keep a 'thread' field
 * and order by that value. This field keeps this data in
 * a way which is easy to update and convenient to use.
 *
 * A "thread" value starts at "1". If we add a child (A) to this comment,
 * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
 * brother of (A) will get "1.2". Next brother of the parent of (A) will get
 * "2" and so on.
 *
 * First of all note that the thread field stores the depth of the comment:
 * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
 *
 * Now to get the ordering right, consider this example:
 *
 * 1
 * 1.1
 * 1.1.1
 * 1.2
 * 2
 *
 * If we "ORDER BY thread ASC" we get the above result, and this is the
 * natural order sorted by time. However, if we "ORDER BY thread DESC"
 * we get:
 *
 * 2
 * 1.2
 * 1.1.1
 * 1.1
 * 1
 *
 * Clearly, this is not a natural way to see a thread, and users will get
 * confused. The natural order to show a thread by time desc would be:
 *
 * 2
 * 1
 * 1.2
 * 1.1
 * 1.1.1
 *
 * which is what we already did before the standard pager patch. To achieve
 * this we simply add a "/" at the end of each "thread" value. This way, the
 * thread fields will look like this:
 *
 * 1/
 * 1.1/
 * 1.1.1/
 * 1.2/
 * 2/
 *
 * we add "/" since this char is, in ASCII, higher than every number, so if
 * now we "ORDER BY thread DESC" we get the correct order. However this would
 * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
 * to consider the trailing "/" so we use a substring only.
 */
 function comment_get_thread($node) {
  $mode = _comment_get_display_setting('mode', $node);
  $comments_per_page = _comment_get_display_setting('comments_per_page', $node);

  $query = db_select('comment', 'c')->extend('PagerDefault');
  $query->addField('c', 'cid');
  $query
    ->condition('c.nid', $node->nid)
    ->addTag('node_access')
    ->limit($comments_per_page);

  $count_query = db_select('comment', 'c');
  $count_query->addExpression('COUNT(*)');
  $count_query
    ->condition('c.nid', $node->nid)
    ->addTag('node_access');

  if (!user_access('administer comments')) {
    $query->condition('c.status', COMMENT_PUBLISHED);
    $count_query->condition('c.status', COMMENT_PUBLISHED);
  }
  if ($mode === COMMENT_MODE_FLAT) {
    $query->orderBy('c.cid', 'ASC');
  }
  else {
    // See comment above. Analysis reveals that this doesn't cost too
    // much. It scales much much better than having the whole comment
    // structure.
    $query->orderBy('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'ASC');
  }

  $query->setCountQuery($count_query);
  $cids = $query->execute()->fetchCol();

  return $cids;
}

/**
 * Loop over comment thread, noting indentation level.
 *
 * @param array $comments
 *   An array of comment objects, keyed by cid.
 * @return
 *   The $comments argument is altered by reference with indentation information.
 */
function comment_prepare_thread(&$comments) {
  // A flag stating if we are still searching for first new comment on the thread.
  $first_new = TRUE;

  // A counter that helps track how indented we are.
  $divs = 0;

  foreach ($comments as $key => $comment) {
    if ($first_new && $comment->new != MARK_READ) {
      // Assign the anchor only for the first new comment. This avoids duplicate
      // id attributes on a page.
      $first_new = FALSE;
      $comment->first_new = TRUE;
    }

    // The $divs element instructs #prefix whether to add an indent div or
    // close existing divs (a negative value).
    $comment->depth = count(explode('.', $comment->thread)) - 1;
    if ($comment->depth > $divs) {
      $comment->divs = 1;
      $divs++;
    }
    else {
      $comment->divs = $comment->depth - $divs;
      while ($comment->depth < $divs) {
        $divs--;
      }
    }
    $comments[$key] = $comment;
  }

  // The final comment must close up some hanging divs
  $comments[$key]->divs_final = $divs;
}

/**
 * Generate an array for rendering the given comment.
 *
 * @param $comment
 *   A comment object.
 * @param $build_mode
 *   Build mode, e.g. 'full', 'teaser'...
 *
 * @return
 *   An array as expected by drupal_render().
 */
function comment_build($comment, $build_mode = 'full') {
  $node = node_load($comment->nid);
  $comment = comment_build_content($comment, $build_mode);

  $build = $comment->content;

  $build += array(
    '#theme' => 'comment',
    '#comment' => $comment,
    '#build_mode' => $build_mode,
  );

  $prefix = '';
  $is_threaded = isset($comment->divs) && _comment_get_display_setting('mode', $node) == COMMENT_MODE_THREADED;

  // Add 'new' anchor if needed.
  if (!empty($comment->first_new)) {
    $prefix .= "<a id=\"new\"></a>\n";
  }

  // Add indentation div or close open divs as needed.
  if ($is_threaded) {
    $prefix .= $comment->divs <= 0 ? str_repeat('</div>', abs($comment->divs)) : "\n" . '<div class="indented">';
  }

  // Add anchor for each comment.
  $prefix .= "<a id=\"comment-$comment->cid\"></a>\n";
  $build['#prefix'] = $prefix;

  // Close all open divs.
  if ($is_threaded && !empty($comment->divs_final)) {
    $build['#suffix'] = str_repeat('</div>', $comment->divs_final);
  }

  return $build;
}

/**
 * Builds a structured array representing the comment's content.
 *
 * The content built for the comment (field values, comments, file attachments or
 * other comment components) will vary depending on the $build_mode parameter.
 *
 * @param $comment
 *   A comment object.
 * @param $build_mode
 *   Build mode, e.g. 'full', 'teaser'...
 * @return
 *   A structured array containing the individual elements
 *   of the comment's content.
 */
function comment_build_content($comment, $build_mode = 'full') {
  if (empty($comment->content)) {
    $comment->content = array();
  }

  // Build comment body.
  $comment->content['comment_body'] = array(
    '#markup' => check_markup($comment->comment, $comment->format, '', FALSE),
  );

822 823
  $comment->content += field_attach_view('comment', $comment, $build_mode);

824 825 826
  if (empty($comment->in_preview)) {
    $comment->content['links']['comment'] = array(
      '#theme' => 'links',
827
      '#links' => comment_links($comment),
828 829 830 831 832 833 834 835 836 837 838 839 840
      '#attributes' => array('class' => 'links inline'),
    );
  }

  // Allow modules to make their own additions to the comment.
  module_invoke_all('comment_view', $comment, $build_mode);

  // Allow modules to modify the structured comment.
  drupal_alter('comment_build', $comment, $build_mode);

  return $comment;
}

841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900
/**
 * Helper function, build links for an individual comment.
 *
 * Adds reply, edit, delete etc. depending on the current user permissions.
 *
 * @param $comment
 *   The comment object.
 * @return
 *   A structured array of links.
 */
function comment_links($comment) {
  $links = array();
  $node = node_load($comment->nid);
  if ($node->comment == COMMENT_NODE_OPEN) {
    if (user_access('administer comments') && user_access('post comments')) {
      $links['comment_delete'] = array(
        'title' => t('delete'),
        'href' => "comment/delete/$comment->cid",
        'html' => TRUE,
      );
      $links['comment_edit'] = array(
        'title' => t('edit'),
        'href' => "comment/edit/$comment->cid",
        'html' => TRUE,
      );
      $links['comment_reply'] = array(
        'title' => t('reply'),
        'href' => "comment/reply/$comment->nid/$comment->cid",
        'html' => TRUE,
      );
      if ($comment->status == COMMENT_NOT_PUBLISHED) {
        $links['comment_approve'] = array(
          'title' => t('approve'),
          'href' => "comment/approve/$comment->cid",
          'html' => TRUE,
        );
      }
    }
    elseif (user_access('post comments')) {
      if (comment_access('edit', $comment)) {
        $links['comment_edit'] = array(
          'title' => t('edit'),
          'href' => "comment/edit/$comment->cid",
          'html' => TRUE,
        );
      }
      $links['comment_reply'] = array(
        'title' => t('reply'),
        'href' => "comment/reply/$comment->nid/$comment->cid",
        'html' => TRUE,
      );
    }
    else {
      $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node);
      $links['comment_forbidden']['html'] = TRUE;
    }
  }
  return $links;
}

901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
/**
 * Construct a drupal_render() style array from an array of loaded comments.
 *
 * @param $comments
 *   An array of comments as returned by comment_load_multiple().
 * @param $build_mode
 *   Build mode, e.g. 'full', 'teaser'...
 * @param $weight
 *   An integer representing the weight of the first comment in the list.
 * @return
 *   An array in the format expected by drupal_render().
 */
function comment_build_multiple($comments, $build_mode = 'full', $weight = 0) {
  $build = array(
    '#sorted' => TRUE,
  );
  foreach ($comments as $comment) {
    $build[$comment->cid] = comment_build($comment, $build_mode);
    $build[$comment->cid]['#weight'] = $weight;
    $weight++;
  }
  return $build;
}

925
/**
926
 * Implement hook_form_FORM_ID_alter().
927
 */
928 929
function comment_form_node_type_form_alter(&$form, $form_state) {
  if (isset($form['identity']['type'])) {
930 931 932 933
    $form['comment'] = array(
      '#type' => 'fieldset',
      '#title' => t('Comment settings'),
      '#collapsible' => TRUE,
934
      '#collapsed' => TRUE,
935 936
    );
    $form['comment']['comment_default_mode'] = array(
937 938 939 940
      '#type' => 'checkbox',
      '#title' => t('Threading'),
      '#default_value' => variable_get('comment_default_mode_' . $form['#node_type']->type, COMMENT_MODE_THREADED),
      '#description' => t('Show comment replies in a threaded list.'),
941 942 943
    );
    $form['comment']['comment_default_per_page'] = array(
      '#type' => 'select',
944
      '#title' => t('Comments per page'),
945
      '#default_value' => variable_get('comment_default_per_page_' . $form['#node_type']->type, 50),
946
      '#options' => _comment_per_page(),
947 948 949 950 951 952 953
    );

    $form['comment']['comment'] = array(
      '#type' => 'select',
      '#title' => t('Default comment setting for new content'),
      '#default_value' => variable_get('comment_' . $form['#node_type']->type, COMMENT_NODE_OPEN),
      '#options' => array(t('Hidden'), t('Closed'), t('Open')),
954 955
    );
    $form['comment']['comment_anonymous'] = array(
956
      '#type' => 'select',
957
      '#title' => t('Anonymous commenting'),
958
      '#default_value' => variable_get('comment_anonymous_' . $form['#node_type']->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT),
959 960 961
      '#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'),
962
        COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information'))
963
    );
964

965
    if (!user_access('post comments', drupal_anonymous_user())) {
966
      $form['comment']['comment_anonymous']['#access'] = FALSE;
967
    }
968

969
    $form['comment']['comment_subject_field'] = array(
970 971
      '#type' => 'checkbox',
      '#title' => t('Allow comment title'),
972
      '#default_value' => variable_get('comment_subject_field_' . $form['#node_type']->type, 1),
973 974
    );
    $form['comment']['comment_form_location'] = array(
975 976 977 978 979 980 981 982
      '#type' => 'checkbox',
      '#title' => t('Show reply form on the same page as comments'),
      '#default_value' => variable_get('comment_form_location_' . $form['#node_type']->type, COMMENT_FORM_BELOW),
    );
    $form['comment']['comment_preview'] = array(
      '#type' => 'checkbox',
      '#title' => t('Require preview'),
      '#default_value' => variable_get('comment_preview_' . $form['#node_type']->type, COMMENT_PREVIEW_OPTIONAL),
983
    );
984
  }
985 986 987
}

/**
988
 * Implement hook_form_alter().
989 990 991
 */
function comment_form_alter(&$form, $form_state, $form_id) {
  if (!empty($form['#node_edit_form'])) {
992 993 994 995 996 997 998
    $node = $form['#node'];
    $form['comment_settings'] = array(
      '#type' => 'fieldset',
      '#access' => user_access('administer comments'),
      '#title' => t('Comment settings'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
999 1000
      '#group' => 'additional_settings',
      '#attached_js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
1001 1002
      '#weight' => 30,
    );
1003 1004
    $comment_count = isset($node->nid) ? db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array(':nid' => $node->nid))->fetchField() : 0;
    $comment_settings = ($node->comment == COMMENT_NODE_HIDDEN && empty($comment_count)) ? COMMENT_NODE_CLOSED : $node->comment;
1005 1006 1007
    $form['comment_settings']['comment'] = array(
      '#type' => 'radios',
      '#parents' => array('comment'),
1008 1009 1010 1011 1012 1013 1014 1015 1016
      '#default_value' => $comment_settings,
      '#options' => array(
        COMMENT_NODE_OPEN => t('Open'),
        COMMENT_NODE_CLOSED => t('Closed'),
        COMMENT_NODE_HIDDEN => t('Hidden'),
      ),
      COMMENT_NODE_OPEN => array(
        '#type' => 'radio',
        '#title' => t('Open'),
1017
        '#description' => theme('indentation') . t('Users with the "Post comments" permission can post comments.'),
1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040
        '#return_value' => COMMENT_NODE_OPEN,
        '#default_value' => $comment_settings,
        '#id' => 'edit-comment-2',
        '#parents' => array('comment'),
      ),
      COMMENT_NODE_CLOSED => array(
        '#type' => 'radio',
        '#title' => t('Closed'),
        '#description' => theme('indentation') . t('Users cannot post comments, but existing comments will be displayed.'),
        '#return_value' => COMMENT_NODE_CLOSED,
        '#default_value' => $comment_settings,
        '#id' => 'edit-comment-1',
        '#parents' => array('comment'),
      ),
      COMMENT_NODE_HIDDEN => array(
        '#type' => 'radio',
        '#title' => t('Hidden'),
        '#description' => theme('indentation') . t('Comments are hidden from view.'),
        '#return_value' => COMMENT_NODE_HIDDEN,
        '#default_value' => $comment_settings,
        '#id' => 'edit-comment-0',
        '#parents' => array('comment'),
      ),
1041
    );
1042 1043 1044 1045 1046 1047 1048
    // If the node doesn't have any comments, the "hidden" option makes no
    // sense, so don't even bother presenting it to the user.
    if (empty($comment_count)) {
      unset($form['comment_settings']['comment']['#options'][COMMENT_NODE_HIDDEN]);
      unset($form['comment_settings']['comment'][COMMENT_NODE_HIDDEN]);
      $form['comment_settings']['comment'][COMMENT_NODE_CLOSED]['#description'] = theme('indentation') . t('Users cannot post comments.');
    }
1049 1050 1051
  }
}

Dries's avatar
 
Dries committed
1052
/**
1053
 * Implement hook_node_load().
Dries's avatar
 
Dries committed
1054
 */
1055
function comment_node_load($nodes, $types) {
1056 1057 1058 1059 1060 1061
  $comments_enabled = array();

  // Check if comments are enabled for each node. If comments are disabled,
  // assign values without hitting the database.
  foreach ($nodes as $node) {
    // Store whether comments are enabled for this node.
1062
    if ($node->commen