Drupal PHP Cross Reference Content Management Systems

Source: /modules/comment/comment.module - 2727 lines - 92392 bytes - Summary - Text - Print

   1  <?php
   2  
   3  /**
   4   * @file
   5   * Enables users to comment on published content.
   6   *
   7   * When enabled, the Drupal comment module creates a discussion
   8   * board for each Drupal node. Users can post comments to discuss
   9   * a forum topic, weblog post, story, collaborative book page, etc.
  10   */
  11  
  12  /**
  13   * Comment is awaiting approval.
  14   */
  15  define('COMMENT_NOT_PUBLISHED', 0);
  16  
  17  /**
  18   * Comment is published.
  19   */
  20  define('COMMENT_PUBLISHED', 1);
  21  
  22  /**
  23   * Comments are displayed in a flat list - expanded.
  24   */
  25  define('COMMENT_MODE_FLAT', 0);
  26  
  27  /**
  28   * Comments are displayed as a threaded list - expanded.
  29   */
  30  define('COMMENT_MODE_THREADED', 1);
  31  
  32  /**
  33   * Anonymous posters cannot enter their contact information.
  34   */
  35  define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0);
  36  
  37  /**
  38   * Anonymous posters may leave their contact information.
  39   */
  40  define('COMMENT_ANONYMOUS_MAY_CONTACT', 1);
  41  
  42  /**
  43   * Anonymous posters are required to leave their contact information.
  44   */
  45  define('COMMENT_ANONYMOUS_MUST_CONTACT', 2);
  46  
  47  /**
  48   * Comment form should be displayed on a separate page.
  49   */
  50  define('COMMENT_FORM_SEPARATE_PAGE', 0);
  51  
  52  /**
  53   * Comment form should be shown below post or list of comments.
  54   */
  55  define('COMMENT_FORM_BELOW', 1);
  56  
  57  /**
  58   * Comments for this node are hidden.
  59   */
  60  define('COMMENT_NODE_HIDDEN', 0);
  61  
  62  /**
  63   * Comments for this node are closed.
  64   */
  65  define('COMMENT_NODE_CLOSED', 1);
  66  
  67  /**
  68   * Comments for this node are open.
  69   */
  70  define('COMMENT_NODE_OPEN', 2);
  71  
  72  /**
  73   * Implements hook_help().
  74   */
  75  function comment_help($path, $arg) {
  76    switch ($path) {
  77      case 'admin/help#comment':
  78        $output = '<h3>' . t('About') . '</h3>';
  79        $output .= '<p>' . t('The Comment module allows users to comment on site content, set commenting defaults and permissions, and moderate comments. For more information, see the online handbook entry for <a href="@comment">Comment module</a>.', array('@comment' => 'http://drupal.org/documentation/modules/comment/')) . '</p>';
  80        $output .= '<h3>' . t('Uses') . '</h3>';
  81        $output .= '<dl>';
  82        $output .= '<dt>' . t('Default and custom settings') . '</dt>';
  83        $output .= '<dd>' . t("Each <a href='@content-type'>content type</a> can have its own default comment settings configured as: <em>Open</em> to allow new comments, <em>Hidden</em> to hide existing comments and prevent new comments, or <em>Closed</em> to view existing comments, but prevent new comments. These defaults will apply to all new content created (changes to the settings on existing content must be done manually). Other comment settings can also be customized per content type, and can be overridden for any given item of content. When a comment has no replies, it remains editable by its author, as long as the author has a user account and is logged in.", array('@content-type' => url('admin/structure/types'))) . '</dd>';
  84        $output .= '<dt>' . t('Comment approval') . '</dt>';
  85        $output .= '<dd>' . t("Comments from users who have the <em>Skip comment approval</em> permission are published immediately. All other comments are placed in the <a href='@comment-approval'>Unapproved comments</a> queue, until a user who has permission to <em>Administer comments</em> publishes or deletes them. Published comments can be bulk managed on the <a href='@admin-comment'>Published comments</a> administration page.", array('@comment-approval' => url('admin/content/comment/approval'), '@admin-comment' => url('admin/content/comment'))) . '</dd>';
  86        $output .= '</dl>';
  87        return $output;
  88    }
  89  }
  90  
  91  /**
  92   * Implements hook_entity_info().
  93   */
  94  function comment_entity_info() {
  95    $return = array(
  96      'comment' => array(
  97        'label' => t('Comment'),
  98        'base table' => 'comment',
  99        'uri callback' => 'comment_uri',
 100        'fieldable' => TRUE,
 101        'controller class' => 'CommentController',
 102        'entity keys' => array(
 103          'id' => 'cid',
 104          'bundle' => 'node_type',
 105          'label' => 'subject',
 106          'language' => 'language',
 107        ),
 108        'bundles' => array(),
 109        'view modes' => array(
 110          'full' => array(
 111            'label' => t('Full comment'),
 112            'custom settings' => FALSE,
 113          ),
 114        ),
 115        'static cache' => FALSE,
 116      ),
 117    );
 118  
 119    foreach (node_type_get_names() as $type => $name) {
 120      $return['comment']['bundles']['comment_node_' . $type] = array(
 121        'label' => t('@node_type comment', array('@node_type' => $name)),
 122        // Provide the node type/bundle name for other modules, so it does not
 123        // have to be extracted manually from the bundle name.
 124        'node bundle' => $type,
 125        'admin' => array(
 126          // Place the Field UI paths for comments one level below the
 127          // corresponding paths for nodes, so that they appear in the same set
 128          // of local tasks. Note that the paths use a different placeholder name
 129          // and thus a different menu loader callback, so that Field UI page
 130          // callbacks get a comment bundle name from the node type in the URL.
 131          // See comment_node_type_load() and comment_menu_alter().
 132          'path' => 'admin/structure/types/manage/%comment_node_type/comment',
 133          'bundle argument' => 4,
 134          'real path' => 'admin/structure/types/manage/' . str_replace('_', '-', $type) . '/comment',
 135          'access arguments' => array('administer content types'),
 136        ),
 137      );
 138    }
 139  
 140    return $return;
 141  }
 142  
 143  /**
 144   * Menu loader callback for Field UI paths.
 145   *
 146   * Return a comment bundle name from a node type in the URL.
 147   */
 148  function comment_node_type_load($name) {
 149    if ($type = node_type_get_type(strtr($name, array('-' => '_')))) {
 150      return 'comment_node_' . $type->type;
 151    }
 152  }
 153  
 154  /**
 155   * Entity URI callback.
 156   */
 157  function comment_uri($comment) {
 158    return array(
 159      'path' => 'comment/' . $comment->cid,
 160      'options' => array('fragment' => 'comment-' . $comment->cid),
 161    );
 162  }
 163  
 164  /**
 165   * Implements hook_field_extra_fields().
 166   */
 167  function comment_field_extra_fields() {
 168    $return = array();
 169  
 170    foreach (node_type_get_types() as $type) {
 171      if (variable_get('comment_subject_field_' . $type->type, 1) == 1) {
 172        $return['comment']['comment_node_' . $type->type] = array(
 173          'form' => array(
 174            'author' => array(
 175              'label' => t('Author'),
 176              'description' => t('Author textfield'),
 177              'weight' => -2,
 178            ),
 179            'subject' => array(
 180              'label' => t('Subject'),
 181              'description' => t('Subject textfield'),
 182              'weight' => -1,
 183            ),
 184          ),
 185        );
 186      }
 187    }
 188  
 189    return $return;
 190  }
 191  
 192  /**
 193   * Implements hook_theme().
 194   */
 195  function comment_theme() {
 196    return array(
 197      'comment_block' => array(
 198        'variables' => array(),
 199      ),
 200      'comment_preview' => array(
 201        'variables' => array('comment' => NULL),
 202      ),
 203      'comment' => array(
 204        'template' => 'comment',
 205        'render element' => 'elements',
 206      ),
 207      'comment_post_forbidden' => array(
 208        'variables' => array('node' => NULL),
 209      ),
 210      'comment_wrapper' => array(
 211        'template' => 'comment-wrapper',
 212        'render element' => 'content',
 213      ),
 214    );
 215  }
 216  
 217  /**
 218   * Implements hook_menu().
 219   */
 220  function comment_menu() {
 221    $items['admin/content/comment'] = array(
 222      'title' => 'Comments',
 223      'description' => 'List and edit site comments and the comment approval queue.',
 224      'page callback' => 'comment_admin',
 225      'access arguments' => array('administer comments'),
 226      'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM,
 227      'file' => 'comment.admin.inc',
 228    );
 229    // Tabs begin here.
 230    $items['admin/content/comment/new'] = array(
 231      'title' => 'Published comments',
 232      'type' => MENU_DEFAULT_LOCAL_TASK,
 233      'weight' => -10,
 234    );
 235    $items['admin/content/comment/approval'] = array(
 236      'title' => 'Unapproved comments',
 237      'title callback' => 'comment_count_unpublished',
 238      'page arguments' => array('approval'),
 239      'access arguments' => array('administer comments'),
 240      'type' => MENU_LOCAL_TASK,
 241    );
 242    $items['comment/%'] = array(
 243      'title' => 'Comment permalink',
 244      'page callback' => 'comment_permalink',
 245      'page arguments' => array(1),
 246      'access arguments' => array('access comments'),
 247    );
 248    $items['comment/%/view'] = array(
 249      'title' => 'View comment',
 250      'type' => MENU_DEFAULT_LOCAL_TASK,
 251      'weight' => -10,
 252    );
 253    // Every other comment path uses %, but this one loads the comment directly,
 254    // so we don't end up loading it twice (in the page and access callback).
 255    $items['comment/%comment/edit'] = array(
 256      'title' => 'Edit',
 257      'page callback' => 'comment_edit_page',
 258      'page arguments' => array(1),
 259      'access callback' => 'comment_access',
 260      'access arguments' => array('edit', 1),
 261      'type' => MENU_LOCAL_TASK,
 262      'weight' => 0,
 263    );
 264    $items['comment/%/approve'] = array(
 265      'title' => 'Approve',
 266      'page callback' => 'comment_approve',
 267      'page arguments' => array(1),
 268      'access arguments' => array('administer comments'),
 269      'file' => 'comment.pages.inc',
 270      'weight' => 1,
 271    );
 272    $items['comment/%/delete'] = array(
 273      'title' => 'Delete',
 274      'page callback' => 'comment_confirm_delete_page',
 275      'page arguments' => array(1),
 276      'access arguments' => array('administer comments'),
 277      'type' => MENU_LOCAL_TASK,
 278      'file' => 'comment.admin.inc',
 279      'weight' => 2,
 280    );
 281    $items['comment/reply/%node'] = array(
 282      'title' => 'Add new comment',
 283      'page callback' => 'comment_reply',
 284      'page arguments' => array(2),
 285      'access callback' => 'node_access',
 286      'access arguments' => array('view', 2),
 287      'file' => 'comment.pages.inc',
 288    );
 289  
 290    return $items;
 291  }
 292  
 293  /**
 294   * Implements hook_menu_alter().
 295   */
 296  function comment_menu_alter(&$items) {
 297    // Add comments to the description for admin/content.
 298    $items['admin/content']['description'] = 'Administer content and comments.';
 299  
 300    // Adjust the Field UI tabs on admin/structure/types/manage/[node-type].
 301    // See comment_entity_info().
 302    $items['admin/structure/types/manage/%comment_node_type/comment/fields']['title'] = 'Comment fields';
 303    $items['admin/structure/types/manage/%comment_node_type/comment/fields']['weight'] = 3;
 304    $items['admin/structure/types/manage/%comment_node_type/comment/display']['title'] = 'Comment display';
 305    $items['admin/structure/types/manage/%comment_node_type/comment/display']['weight'] = 4;
 306  }
 307  
 308  /**
 309   * Returns a menu title which includes the number of unapproved comments.
 310   */
 311  function comment_count_unpublished() {
 312    $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE status = :status', array(
 313      ':status' => COMMENT_NOT_PUBLISHED,
 314    ))->fetchField();
 315    return t('Unapproved comments (@count)', array('@count' => $count));
 316  }
 317  
 318  /**
 319   * Implements hook_node_type_insert().
 320   *
 321   * Creates a comment body field for a node type created while the comment module
 322   * is enabled. For node types created before the comment module is enabled,
 323   * hook_modules_enabled() serves to create the body fields.
 324   *
 325   * @see comment_modules_enabled()
 326   */
 327  function comment_node_type_insert($info) {
 328    _comment_body_field_create($info);
 329  }
 330  
 331  /**
 332   * Implements hook_node_type_update().
 333   */
 334  function comment_node_type_update($info) {
 335    if (!empty($info->old_type) && $info->type != $info->old_type) {
 336      field_attach_rename_bundle('comment', 'comment_node_' . $info->old_type, 'comment_node_' . $info->type);
 337    }
 338  }
 339  
 340  /**
 341   * Implements hook_node_type_delete().
 342   */
 343  function comment_node_type_delete($info) {
 344    field_attach_delete_bundle('comment', 'comment_node_' . $info->type);
 345    $settings = array(
 346      'comment',
 347      'comment_default_mode',
 348      'comment_default_per_page',
 349      'comment_anonymous',
 350      'comment_subject_field',
 351      'comment_preview',
 352      'comment_form_location',
 353    );
 354    foreach ($settings as $setting) {
 355      variable_del($setting . '_' . $info->type);
 356    }
 357  }
 358  
 359   /**
 360   * Creates a comment_body field instance for a given node type.
 361   */
 362  function _comment_body_field_create($info) {
 363    // Create the field if needed.
 364    if (!field_read_field('comment_body', array('include_inactive' => TRUE))) {
 365      $field = array(
 366        'field_name' => 'comment_body',
 367        'type' => 'text_long',
 368        'entity_types' => array('comment'),
 369      );
 370      field_create_field($field);
 371    }
 372    // Create the instance if needed.
 373    if (!field_read_instance('comment', 'comment_body', 'comment_node_' . $info->type, array('include_inactive' => TRUE))) {
 374      field_attach_create_bundle('comment', 'comment_node_' . $info->type);
 375      // Attaches the body field by default.
 376      $instance = array(
 377        'field_name' => 'comment_body',
 378        'label' => 'Comment',
 379        'entity_type' => 'comment',
 380        'bundle' => 'comment_node_' . $info->type,
 381        'settings' => array('text_processing' => 1),
 382        'required' => TRUE,
 383        'display' => array(
 384          'default' => array(
 385            'label' => 'hidden',
 386            'type' => 'text_default',
 387            'weight' => 0,
 388          ),
 389        ),
 390      );
 391      field_create_instance($instance);
 392    }
 393  }
 394  
 395  /**
 396   * Implements hook_permission().
 397   */
 398  function comment_permission() {
 399    return array(
 400      'administer comments' => array(
 401        'title' => t('Administer comments and comment settings'),
 402      ),
 403      'access comments' => array(
 404        'title' => t('View comments'),
 405      ),
 406      'post comments' => array(
 407        'title' => t('Post comments'),
 408      ),
 409      'skip comment approval' => array(
 410        'title' => t('Skip comment approval'),
 411      ),
 412      'edit own comments' => array(
 413        'title' => t('Edit own comments'),
 414      ),
 415    );
 416  }
 417  
 418  /**
 419   * Implements hook_block_info().
 420   */
 421  function comment_block_info() {
 422    $blocks['recent']['info'] = t('Recent comments');
 423    $blocks['recent']['properties']['administrative'] = TRUE;
 424  
 425    return $blocks;
 426  }
 427  
 428  /**
 429   * Implements hook_block_configure().
 430   */
 431  function comment_block_configure($delta = '') {
 432    $form['comment_block_count'] = array(
 433      '#type' => 'select',
 434      '#title' => t('Number of recent comments'),
 435      '#default_value' => variable_get('comment_block_count', 10),
 436      '#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)),
 437    );
 438  
 439    return $form;
 440  }
 441  
 442  /**
 443   * Implements hook_block_save().
 444   */
 445  function comment_block_save($delta = '', $edit = array()) {
 446    variable_set('comment_block_count', (int) $edit['comment_block_count']);
 447  }
 448  
 449  /**
 450   * Implements hook_block_view().
 451   *
 452   * Generates a block with the most recent comments.
 453   */
 454  function comment_block_view($delta = '') {
 455    if (user_access('access comments')) {
 456      $block['subject'] = t('Recent comments');
 457      $block['content'] = theme('comment_block');
 458  
 459      return $block;
 460    }
 461  }
 462  
 463  /**
 464   * Redirects comment links to the correct page depending on comment settings.
 465   *
 466   * Since comments are paged there is no way to guarantee which page a comment
 467   * appears on. Comment paging and threading settings may be changed at any time.
 468   * With threaded comments, an individual comment may move between pages as
 469   * comments can be added either before or after it in the overall discussion.
 470   * Therefore we use a central routing function for comment links, which
 471   * calculates the page number based on current comment settings and returns
 472   * the full comment view with the pager set dynamically.
 473   *
 474   * @param $cid
 475   *   A comment identifier.
 476   * @return
 477   *   The comment listing set to the page on which the comment appears.
 478   */
 479  function comment_permalink($cid) {
 480    if (($comment = comment_load($cid)) && ($node = node_load($comment->nid))) {
 481  
 482      // Find the current display page for this comment.
 483      $page = comment_get_display_page($comment->cid, $node->type);
 484  
 485      // Set $_GET['q'] and $_GET['page'] ourselves so that the node callback
 486      // behaves as it would when visiting the page directly.
 487      $_GET['q'] = 'node/' . $node->nid;
 488      $_GET['page'] = $page;
 489  
 490      // Return the node view, this will show the correct comment in context.
 491      return menu_execute_active_handler('node/' . $node->nid, FALSE);
 492    }
 493    drupal_not_found();
 494  }
 495  
 496  /**
 497   * Find the most recent comments that are available to the current user.
 498   *
 499   * @param integer $number
 500   *   (optional) The maximum number of comments to find. Defaults to 10.
 501   *
 502   * @return
 503   *   An array of comment objects or an empty array if there are no recent
 504   *   comments visible to the current user.
 505   */
 506  function comment_get_recent($number = 10) {
 507    $query = db_select('comment', 'c');
 508    $query->innerJoin('node', 'n', 'n.nid = c.nid');
 509    $query->addTag('node_access');
 510    $comments = $query
 511      ->fields('c')
 512      ->condition('c.status', COMMENT_PUBLISHED)
 513      ->condition('n.status', NODE_PUBLISHED)
 514      ->orderBy('c.created', 'DESC')
 515      // Additionally order by cid to ensure that comments with the same timestamp
 516      // are returned in the exact order posted.
 517      ->orderBy('c.cid', 'DESC')
 518      ->range(0, $number)
 519      ->execute()
 520      ->fetchAll();
 521  
 522    return $comments ? $comments : array();
 523  }
 524  
 525  /**
 526   * Calculate page number for first new comment.
 527   *
 528   * @param $num_comments
 529   *   Number of comments.
 530   * @param $new_replies
 531   *   Number of new replies.
 532   * @param $node
 533   *   The first new comment node.
 534   * @return
 535   *   "page=X" if the page number is greater than zero; empty string otherwise.
 536   */
 537  function comment_new_page_count($num_comments, $new_replies, $node) {
 538    $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
 539    $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
 540    $pagenum = NULL;
 541    $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
 542    if ($num_comments <= $comments_per_page) {
 543      // Only one page of comments.
 544      $pageno = 0;
 545    }
 546    elseif ($flat) {
 547      // Flat comments.
 548      $count = $num_comments - $new_replies;
 549      $pageno = $count / $comments_per_page;
 550    }
 551    else {
 552      // Threaded comments: we build a query with a subquery to find the first
 553      // thread with a new comment.
 554  
 555      // 1. Find all the threads with a new comment.
 556      $unread_threads_query = db_select('comment')
 557        ->fields('comment', array('thread'))
 558        ->condition('nid', $node->nid)
 559        ->condition('status', COMMENT_PUBLISHED)
 560        ->orderBy('created', 'DESC')
 561        ->orderBy('cid', 'DESC')
 562        ->range(0, $new_replies);
 563  
 564      // 2. Find the first thread.
 565      $first_thread = db_select($unread_threads_query, 'thread')
 566        ->fields('thread', array('thread'))
 567        ->orderBy('SUBSTRING(thread, 1, (LENGTH(thread) - 1))')
 568        ->range(0, 1)
 569        ->execute()
 570        ->fetchField();
 571  
 572      // Remove the final '/'.
 573      $first_thread = substr($first_thread, 0, -1);
 574  
 575      // Find the number of the first comment of the first unread thread.
 576      $count = db_query('SELECT COUNT(*) FROM {comment} WHERE nid = :nid AND status = :status AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread', array(
 577        ':status' => COMMENT_PUBLISHED,
 578        ':nid' => $node->nid,
 579        ':thread' => $first_thread,
 580      ))->fetchField();
 581  
 582      $pageno = $count / $comments_per_page;
 583    }
 584  
 585    if ($pageno >= 1) {
 586      $pagenum = array('page' => intval($pageno));
 587    }
 588  
 589    return $pagenum;
 590  }
 591  
 592  /**
 593   * Returns HTML for a list of recent comments to be displayed in the comment block.
 594   *
 595   * @ingroup themeable
 596   */
 597  function theme_comment_block() {
 598    $items = array();
 599    $number = variable_get('comment_block_count', 10);
 600    foreach (comment_get_recent($number) as $comment) {
 601      $items[] = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)) . '&nbsp;<span>' . t('@time ago', array('@time' => format_interval(REQUEST_TIME - $comment->changed))) . '</span>';
 602    }
 603  
 604    if ($items) {
 605      return theme('item_list', array('items' => $items));
 606    }
 607    else {
 608      return t('No comments available.');
 609    }
 610  }
 611  
 612  /**
 613   * Implements hook_node_view().
 614   */
 615  function comment_node_view($node, $view_mode) {
 616    $links = array();
 617  
 618    if ($node->comment != COMMENT_NODE_HIDDEN) {
 619      if ($view_mode == 'rss') {
 620        // Add a comments RSS element which is a URL to the comments of this node.
 621        $node->rss_elements[] = array(
 622          'key' => 'comments',
 623          'value' => url('node/' . $node->nid, array('fragment' => 'comments', 'absolute' => TRUE))
 624        );
 625      }
 626      elseif ($view_mode == 'teaser') {
 627        // Teaser view: display the number of comments that have been posted,
 628        // or a link to add new comments if the user has permission, the node
 629        // is open to new comments, and there currently are none.
 630        if (user_access('access comments')) {
 631          if (!empty($node->comment_count)) {
 632            $links['comment-comments'] = array(
 633              'title' => format_plural($node->comment_count, '1 comment', '@count comments'),
 634              'href' => "node/$node->nid",
 635              'attributes' => array('title' => t('Jump to the first comment of this posting.')),
 636              'fragment' => 'comments',
 637              'html' => TRUE,
 638            );
 639            // Show a link to the first new comment.
 640            if ($new = comment_num_new($node->nid)) {
 641              $links['comment-new-comments'] = array(
 642                'title' => format_plural($new, '1 new comment', '@count new comments'),
 643                'href' => "node/$node->nid",
 644                'query' => comment_new_page_count($node->comment_count, $new, $node),
 645                'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
 646                'fragment' => 'new',
 647                'html' => TRUE,
 648              );
 649            }
 650          }
 651        }
 652        if ($node->comment == COMMENT_NODE_OPEN) {
 653          if (user_access('post comments')) {
 654            $links['comment-add'] = array(
 655              'title' => t('Add new comment'),
 656              'href' => "comment/reply/$node->nid",
 657              'attributes' => array('title' => t('Add a new comment to this page.')),
 658              'fragment' => 'comment-form',
 659            );
 660          }
 661          else {
 662            $links['comment_forbidden'] = array(
 663              'title' => theme('comment_post_forbidden', array('node' => $node)),
 664              'html' => TRUE,
 665            );
 666          }
 667        }
 668      }
 669      elseif ($view_mode != 'search_index' && $view_mode != 'search_result') {
 670        // Node in other view modes: add a "post comment" link if the user is
 671        // allowed to post comments and if this node is allowing new comments.
 672        // But we don't want this link if we're building the node for search
 673        // indexing or constructing a search result excerpt.
 674        if ($node->comment == COMMENT_NODE_OPEN) {
 675          $comment_form_location = variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW);
 676          if (user_access('post comments')) {
 677            // Show the "post comment" link if the form is on another page, or
 678            // if there are existing comments that the link will skip past.
 679            if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE || (!empty($node->comment_count) && user_access('access comments'))) {
 680              $links['comment-add'] = array(
 681                'title' => t('Add new comment'),
 682                'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
 683                'href' => "node/$node->nid",
 684                'fragment' => 'comment-form',
 685              );
 686              if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
 687                $links['comment-add']['href'] = "comment/reply/$node->nid";
 688              }
 689            }
 690          }
 691          else {
 692            $links['comment_forbidden'] = array(
 693              'title' => theme('comment_post_forbidden', array('node' => $node)),
 694              'html' => TRUE,
 695            );
 696          }
 697        }
 698      }
 699  
 700      $node->content['links']['comment'] = array(
 701        '#theme' => 'links__node__comment',
 702        '#links' => $links,
 703        '#attributes' => array('class' => array('links', 'inline')),
 704      );
 705  
 706      // Only append comments when we are building a node on its own node detail
 707      // page. We compare $node and $page_node to ensure that comments are not
 708      // appended to other nodes shown on the page, for example a node_reference
 709      // displayed in 'full' view mode within another node.
 710      if ($node->comment && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) {
 711        $node->content['comments'] = comment_node_page_additions($node);
 712      }
 713    }
 714  }
 715  
 716  /**
 717   * Build the comment-related elements for node detail pages.
 718   *
 719   * @param $node
 720   *  A node object.
 721   */
 722  function comment_node_page_additions($node) {
 723    $additions = array();
 724  
 725    // Only attempt to render comments if the node has visible comments.
 726    // Unpublished comments are not included in $node->comment_count, so show
 727    // comments unconditionally if the user is an administrator.
 728    if (($node->comment_count && user_access('access comments')) || user_access('administer comments')) {
 729      $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
 730      $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
 731      if ($cids = comment_get_thread($node, $mode, $comments_per_page)) {
 732        $comments = comment_load_multiple($cids);
 733        comment_prepare_thread($comments);
 734        $build = comment_view_multiple($comments, $node);
 735        $build['pager']['#theme'] = 'pager';
 736        $additions['comments'] = $build;
 737      }
 738    }
 739  
 740    // Append comment form if needed.
 741    if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) {
 742      $build = drupal_get_form("comment_node_{$node->type}_form", (object) array('nid' => $node->nid));
 743      $additions['comment_form'] = $build;
 744    }
 745  
 746    if ($additions) {
 747      $additions += array(
 748        '#theme' => 'comment_wrapper__node_' . $node->type,
 749        '#node' => $node,
 750        'comments' => array(),
 751        'comment_form' => array(),
 752      );
 753    }
 754  
 755    return $additions;
 756  }
 757  
 758  /**
 759   * Retrieve comments for a thread.
 760   *
 761   * @param $node
 762   *   The node whose comment(s) needs rendering.
 763   * @param $mode
 764   *   The comment display mode; COMMENT_MODE_FLAT or COMMENT_MODE_THREADED.
 765   * @param $comments_per_page
 766   *   The amount of comments to display per page.
 767   *
 768   * To display threaded comments in the correct order we keep a 'thread' field
 769   * and order by that value. This field keeps this data in
 770   * a way which is easy to update and convenient to use.
 771   *
 772   * A "thread" value starts at "1". If we add a child (A) to this comment,
 773   * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
 774   * brother of (A) will get "1.2". Next brother of the parent of (A) will get
 775   * "2" and so on.
 776   *
 777   * First of all note that the thread field stores the depth of the comment:
 778   * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
 779   *
 780   * Now to get the ordering right, consider this example:
 781   *
 782   * 1
 783   * 1.1
 784   * 1.1.1
 785   * 1.2
 786   * 2
 787   *
 788   * If we "ORDER BY thread ASC" we get the above result, and this is the
 789   * natural order sorted by time. However, if we "ORDER BY thread DESC"
 790   * we get:
 791   *
 792   * 2
 793   * 1.2
 794   * 1.1.1
 795   * 1.1
 796   * 1
 797   *
 798   * Clearly, this is not a natural way to see a thread, and users will get
 799   * confused. The natural order to show a thread by time desc would be:
 800   *
 801   * 2
 802   * 1
 803   * 1.2
 804   * 1.1
 805   * 1.1.1
 806   *
 807   * which is what we already did before the standard pager patch. To achieve
 808   * this we simply add a "/" at the end of each "thread" value. This way, the
 809   * thread fields will look like this:
 810   *
 811   * 1/
 812   * 1.1/
 813   * 1.1.1/
 814   * 1.2/
 815   * 2/
 816   *
 817   * we add "/" since this char is, in ASCII, higher than every number, so if
 818   * now we "ORDER BY thread DESC" we get the correct order. However this would
 819   * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
 820   * to consider the trailing "/" so we use a substring only.
 821   */
 822  function comment_get_thread($node, $mode, $comments_per_page) {
 823    $query = db_select('comment', 'c')->extend('PagerDefault');
 824    $query->addField('c', 'cid');
 825    $query
 826      ->condition('c.nid', $node->nid)
 827      ->addTag('node_access')
 828      ->addTag('comment_filter')
 829      ->addMetaData('node', $node)
 830      ->limit($comments_per_page);
 831  
 832    $count_query = db_select('comment', 'c');
 833    $count_query->addExpression('COUNT(*)');
 834    $count_query
 835      ->condition('c.nid', $node->nid)
 836      ->addTag('node_access')
 837      ->addTag('comment_filter')
 838      ->addMetaData('node', $node);
 839  
 840    if (!user_access('administer comments')) {
 841      $query->condition('c.status', COMMENT_PUBLISHED);
 842      $count_query->condition('c.status', COMMENT_PUBLISHED);
 843    }
 844    if ($mode === COMMENT_MODE_FLAT) {
 845      $query->orderBy('c.cid', 'ASC');
 846    }
 847    else {
 848      // See comment above. Analysis reveals that this doesn't cost too
 849      // much. It scales much much better than having the whole comment
 850      // structure.
 851      $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
 852      $query->orderBy('torder', 'ASC');
 853    }
 854  
 855    $query->setCountQuery($count_query);
 856    $cids = $query->execute()->fetchCol();
 857  
 858    return $cids;
 859  }
 860  
 861  /**
 862   * Loop over comment thread, noting indentation level.
 863   *
 864   * @param array $comments
 865   *   An array of comment objects, keyed by cid.
 866   * @return
 867   *   The $comments argument is altered by reference with indentation information.
 868   */
 869  function comment_prepare_thread(&$comments) {
 870    // A flag stating if we are still searching for first new comment on the thread.
 871    $first_new = TRUE;
 872  
 873    // A counter that helps track how indented we are.
 874    $divs = 0;
 875  
 876    foreach ($comments as $key => $comment) {
 877      if ($first_new && $comment->new != MARK_READ) {
 878        // Assign the anchor only for the first new comment. This avoids duplicate
 879        // id attributes on a page.
 880        $first_new = FALSE;
 881        $comment->first_new = TRUE;
 882      }
 883  
 884      // The $divs element instructs #prefix whether to add an indent div or
 885      // close existing divs (a negative value).
 886      $comment->depth = count(explode('.', $comment->thread)) - 1;
 887      if ($comment->depth > $divs) {
 888        $comment->divs = 1;
 889        $divs++;
 890      }
 891      else {
 892        $comment->divs = $comment->depth - $divs;
 893        while ($comment->depth < $divs) {
 894          $divs--;
 895        }
 896      }
 897      $comments[$key] = $comment;
 898    }
 899  
 900    // The final comment must close up some hanging divs
 901    $comments[$key]->divs_final = $divs;
 902  }
 903  
 904  /**
 905   * Generate an array for rendering the given comment.
 906   *
 907   * @param $comment
 908   *   A comment object.
 909   * @param $node
 910   *   The node the comment is attached to.
 911   * @param $view_mode
 912   *   View mode, e.g. 'full', 'teaser'...
 913   * @param $langcode
 914   *   (optional) A language code to use for rendering. Defaults to the global
 915   *   content language of the current request.
 916   *
 917   * @return
 918   *   An array as expected by drupal_render().
 919   */
 920  function comment_view($comment, $node, $view_mode = 'full', $langcode = NULL) {
 921    if (!isset($langcode)) {
 922      $langcode = $GLOBALS['language_content']->language;
 923    }
 924  
 925    // Populate $comment->content with a render() array.
 926    comment_build_content($comment, $node, $view_mode, $langcode);
 927  
 928    $build = $comment->content;
 929    // We don't need duplicate rendering info in comment->content.
 930    unset($comment->content);
 931  
 932    $build += array(
 933      '#theme' => 'comment__node_' . $node->type,
 934      '#comment' => $comment,
 935      '#node' => $node,
 936      '#view_mode' => $view_mode,
 937      '#language' => $langcode,
 938    );
 939  
 940    if (empty($comment->in_preview)) {
 941      $prefix = '';
 942      $is_threaded = isset($comment->divs) && variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED) == COMMENT_MODE_THREADED;
 943  
 944      // Add 'new' anchor if needed.
 945      if (!empty($comment->first_new)) {
 946        $prefix .= "<a id=\"new\"></a>\n";
 947      }
 948  
 949      // Add indentation div or close open divs as needed.
 950      if ($is_threaded) {
 951        $prefix .= $comment->divs <= 0 ? str_repeat('</div>', abs($comment->divs)) : "\n" . '<div class="indented">';
 952      }
 953  
 954      // Add anchor for each comment.
 955      $prefix .= "<a id=\"comment-$comment->cid\"></a>\n";
 956      $build['#prefix'] = $prefix;
 957  
 958      // Close all open divs.
 959      if ($is_threaded && !empty($comment->divs_final)) {
 960        $build['#suffix'] = str_repeat('</div>', $comment->divs_final);
 961      }
 962    }
 963  
 964    // Allow modules to modify the structured comment.
 965    $type = 'comment';
 966    drupal_alter(array('comment_view', 'entity_view'), $build, $type);
 967  
 968    return $build;
 969  }
 970  
 971  /**
 972   * Builds a structured array representing the comment's content.
 973   *
 974   * The content built for the comment (field values, comments, file attachments or
 975   * other comment components) will vary depending on the $view_mode parameter.
 976   *
 977   * @param $comment
 978   *   A comment object.
 979   * @param $node
 980   *   The node the comment is attached to.
 981   * @param $view_mode
 982   *   View mode, e.g. 'full', 'teaser'...
 983   * @param $langcode
 984   *   (optional) A language code to use for rendering. Defaults to the global
 985   *   content language of the current request.
 986   */
 987  function comment_build_content($comment, $node, $view_mode = 'full', $langcode = NULL) {
 988    if (!isset($langcode)) {
 989      $langcode = $GLOBALS['language_content']->language;
 990    }
 991  
 992    // Remove previously built content, if exists.
 993    $comment->content = array();
 994  
 995    // Allow modules to change the view mode.
 996    $context = array(
 997      'entity_type' => 'comment',
 998      'entity' => $comment,
 999      'langcode' => $langcode,
1000    );
1001    drupal_alter('entity_view_mode', $view_mode, $context);
1002  
1003    // Build fields content.
1004    field_attach_prepare_view('comment', array($comment->cid => $comment), $view_mode, $langcode);
1005    entity_prepare_view('comment', array($comment->cid => $comment), $langcode);
1006    $comment->content += field_attach_view('comment', $comment, $view_mode, $langcode);
1007  
1008    $comment->content['links'] = array(
1009      '#theme' => 'links__comment',
1010      '#pre_render' => array('drupal_pre_render_links'),
1011      '#attributes' => array('class' => array('links', 'inline')),
1012    );
1013    if (empty($comment->in_preview)) {
1014      $comment->content['links']['comment'] = array(
1015        '#theme' => 'links__comment__comment',
1016        '#links' => comment_links($comment, $node),
1017        '#attributes' => array('class' => array('links', 'inline')),
1018      );
1019    }
1020  
1021    // Allow modules to make their own additions to the comment.
1022    module_invoke_all('comment_view', $comment, $view_mode, $langcode);
1023    module_invoke_all('entity_view', $comment, 'comment', $view_mode, $langcode);
1024  
1025    // Make sure the current view mode is stored if no module has already
1026    // populated the related key.
1027    $comment->content += array('#view_mode' => $view_mode);
1028  }
1029  
1030  /**
1031   * Helper function, build links for an individual comment.
1032   *
1033   * Adds reply, edit, delete etc. depending on the current user permissions.
1034   *
1035   * @param $comment
1036   *   The comment object.
1037   * @param $node
1038   *   The node the comment is attached to.
1039   * @return
1040   *   A structured array of links.
1041   */
1042  function comment_links($comment, $node) {
1043    $links = array();
1044    if ($node->comment == COMMENT_NODE_OPEN) {
1045      if (user_access('administer comments') && user_access('post comments')) {
1046        $links['comment-delete'] = array(
1047          'title' => t('delete'),
1048          'href' => "comment/$comment->cid/delete",
1049          'html' => TRUE,
1050        );
1051        $links['comment-edit'] = array(
1052          'title' => t('edit'),
1053          'href' => "comment/$comment->cid/edit",
1054          'html' => TRUE,
1055        );
1056        $links['comment-reply'] = array(
1057          'title' => t('reply'),
1058          'href' => "comment/reply/$comment->nid/$comment->cid",
1059          'html' => TRUE,
1060        );
1061        if ($comment->status == COMMENT_NOT_PUBLISHED) {
1062          $links['comment-approve'] = array(
1063            'title' => t('approve'),
1064            'href' => "comment/$comment->cid/approve",
1065            'html' => TRUE,
1066            'query' => array('token' => drupal_get_token("comment/$comment->cid/approve")),
1067          );
1068        }
1069      }
1070      elseif (user_access('post comments')) {
1071        if (comment_access('edit', $comment)) {
1072          $links['comment-edit'] = array(
1073            'title' => t('edit'),
1074            'href' => "comment/$comment->cid/edit",
1075            'html' => TRUE,
1076          );
1077        }
1078        $links['comment-reply'] = array(
1079          'title' => t('reply'),
1080          'href' => "comment/reply/$comment->nid/$comment->cid",
1081          'html' => TRUE,
1082        );
1083      }
1084      else {
1085        $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node));
1086        $links['comment_forbidden']['html'] = TRUE;
1087      }
1088    }
1089    return $links;
1090  }
1091  
1092  /**
1093   * Construct a drupal_render() style array from an array of loaded comments.
1094   *
1095   * @param $comments
1096   *   An array of comments as returned by comment_load_multiple().
1097   * @param $node
1098   *   The node the comments are attached to.
1099   * @param $view_mode
1100   *   View mode, e.g. 'full', 'teaser'...
1101   * @param $weight
1102   *   An integer representing the weight of the first comment in the list.
1103   * @param $langcode
1104   *   A string indicating the language field values are to be shown in. If no
1105   *   language is provided the current content language is used.
1106   *
1107   * @return
1108   *   An array in the format expected by drupal_render().
1109   */
1110  function comment_view_multiple($comments, $node, $view_mode = 'full', $weight = 0, $langcode = NULL) {
1111    field_attach_prepare_view('comment', $comments, $view_mode, $langcode);
1112    entity_prepare_view('comment', $comments, $langcode);
1113  
1114    $build = array(
1115      '#sorted' => TRUE,
1116    );
1117    foreach ($comments as $comment) {
1118      $build[$comment->cid] = comment_view($comment, $node, $view_mode, $langcode);
1119      $build[$comment->cid]['#weight'] = $weight;
1120      $weight++;
1121    }
1122    return $build;
1123  }
1124  
1125  /**
1126   * Implements hook_form_FORM_ID_alter().
1127   */
1128  function comment_form_node_type_form_alter(&$form, $form_state) {
1129    if (isset($form['type'])) {
1130      $form['comment'] = array(
1131        '#type' => 'fieldset',
1132        '#title' => t('Comment settings'),
1133        '#collapsible' => TRUE,
1134        '#collapsed' => TRUE,
1135        '#group' => 'additional_settings',
1136        '#attributes' => array(
1137          'class' => array('comment-node-type-settings-form'),
1138        ),
1139        '#attached' => array(
1140          'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
1141        ),
1142      );
1143      // Unlike coment_form_node_form_alter(), all of these settings are applied
1144      // as defaults to all new nodes. Therefore, it would be wrong to use #states
1145      // to hide the other settings based on the primary comment setting.
1146      $form['comment']['comment'] = array(
1147        '#type' => 'select',
1148        '#title' => t('Default comment setting for new content'),
1149        '#default_value' => variable_get('comment_' . $form['#node_type']->type, COMMENT_NODE_OPEN),
1150        '#options' => array(
1151          COMMENT_NODE_OPEN => t('Open'),
1152          COMMENT_NODE_CLOSED => t('Closed'),
1153          COMMENT_NODE_HIDDEN => t('Hidden'),
1154        ),
1155      );
1156      $form['comment']['comment_default_mode'] = array(
1157        '#type' => 'checkbox',
1158        '#title' => t('Threading'),
1159        '#default_value' => variable_get('comment_default_mode_' . $form['#node_type']->type, COMMENT_MODE_THREADED),
1160        '#description' => t('Show comment replies in a threaded list.'),
1161      );
1162      $form['comment']['comment_default_per_page'] = array(
1163        '#type' => 'select',
1164        '#title' => t('Comments per page'),
1165        '#default_value' => variable_get('comment_default_per_page_' . $form['#node_type']->type, 50),
1166        '#options' => _comment_per_page(),
1167      );
1168      $form['comment']['comment_anonymous'] = array(
1169        '#type' => 'select',
1170        '#title' => t('Anonymous commenting'),
1171        '#default_value' => variable_get('comment_anonymous_' . $form['#node_type']->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT),
1172        '#options' => array(
1173          COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'),
1174          COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'),
1175          COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information'),
1176        ),
1177        '#access' => user_access('post comments', drupal_anonymous_user()),
1178      );
1179      $form['comment']['comment_subject_field'] = array(
1180        '#type' => 'checkbox',
1181        '#title' => t('Allow comment title'),
1182        '#default_value' => variable_get('comment_subject_field_' . $form['#node_type']->type, 1),
1183      );
1184      $form['comment']['comment_form_location'] = array(
1185        '#type' => 'checkbox',
1186        '#title' => t('Show reply form on the same page as comments'),
1187        '#default_value' => variable_get('comment_form_location_' . $form['#node_type']->type, COMMENT_FORM_BELOW),
1188      );
1189      $form['comment']['comment_preview'] = array(
1190        '#type' => 'radios',
1191        '#title' => t('Preview comment'),
1192        '#default_value' => variable_get('comment_preview_' . $form['#node_type']->type, DRUPAL_OPTIONAL),
1193        '#options' => array(
1194          DRUPAL_DISABLED => t('Disabled'),
1195          DRUPAL_OPTIONAL => t('Optional'),
1196          DRUPAL_REQUIRED => t('Required'),
1197        ),
1198      );
1199    }
1200  }
1201  
1202  /**
1203   * Implements hook_form_BASE_FORM_ID_alter().
1204   */
1205  function comment_form_node_form_alter(&$form, $form_state) {
1206    $node = $form['#node'];
1207    $form['comment_settings'] = array(
1208      '#type' => 'fieldset',
1209      '#access' => user_access('administer comments'),
1210      '#title' => t('Comment settings'),
1211      '#collapsible' => TRUE,
1212      '#collapsed' => TRUE,
1213      '#group' => 'additional_settings',
1214      '#attributes' => array(
1215        'class' => array('comment-node-settings-form'),
1216      ),
1217      '#attached' => array(
1218        'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
1219       ),
1220      '#weight' => 30,
1221    );
1222    $comment_count = isset($node->nid) ? db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array(':nid' => $node->nid))->fetchField() : 0;
1223    $comment_settings = ($node->comment == COMMENT_NODE_HIDDEN && empty($comment_count)) ? COMMENT_NODE_CLOSED : $node->comment;
1224    $form['comment_settings']['comment'] = array(
1225      '#type' => 'radios',
1226      '#title' => t('Comments'),
1227      '#title_display' => 'invisible',
1228      '#parents' => array('comment'),
1229      '#default_value' => $comment_settings,
1230      '#options' => array(
1231        COMMENT_NODE_OPEN => t('Open'),
1232        COMMENT_NODE_CLOSED => t('Closed'),
1233        COMMENT_NODE_HIDDEN => t('Hidden'),
1234      ),
1235      COMMENT_NODE_OPEN => array(
1236        '#description' => t('Users with the "Post comments" permission can post comments.'),
1237      ),
1238      COMMENT_NODE_CLOSED => array(
1239        '#description' => t('Users cannot post comments, but existing comments will be displayed.'),
1240      ),
1241      COMMENT_NODE_HIDDEN => array(
1242        '#description' => t('Comments are hidden from view.'),
1243      ),
1244    );
1245    // If the node doesn't have any comments, the "hidden" option makes no
1246    // sense, so don't even bother presenting it to the user.
1247    if (empty($comment_count)) {
1248      $form['comment_settings']['comment'][COMMENT_NODE_HIDDEN]['#access'] = FALSE;
1249      // Also adjust the description of the "closed" option.
1250      $form['comment_settings']['comment'][COMMENT_NODE_CLOSED]['#description'] = t('Users cannot post comments.');
1251    }
1252  }
1253  
1254  /**
1255   * Implements hook_node_load().
1256   */
1257  function comment_node_load($nodes, $types) {
1258    $comments_enabled = array();
1259  
1260    // Check if comments are enabled for each node. If comments are disabled,
1261    // assign values without hitting the database.
1262    foreach ($nodes as $node) {
1263      // Store whether comments are enabled for this node.
1264      if ($node->comment != COMMENT_NODE_HIDDEN) {
1265        $comments_enabled[] = $node->nid;
1266      }
1267      else {
1268        $node->cid = 0;
1269        $node->last_comment_timestamp = $node->created;
1270        $node->last_comment_name = '';
1271        $node->last_comment_uid = $node->uid;
1272        $node->comment_count = 0;
1273      }
1274    }
1275  
1276    // For nodes with comments enabled, fetch information from the database.
1277    if (!empty($comments_enabled)) {
1278      $result = db_query('SELECT nid, cid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count FROM {node_comment_statistics} WHERE nid IN (:comments_enabled)', array(':comments_enabled' => $comments_enabled));
1279      foreach ($result as $record) {
1280        $nodes[$record->nid]->cid = $record->cid;
1281        $nodes[$record->nid]->last_comment_timestamp = $record->last_comment_timestamp;
1282        $nodes[$record->nid]->last_comment_name = $record->last_comment_name;
1283        $nodes[$record->nid]->last_comment_uid = $record->last_comment_uid;
1284        $nodes[$record->nid]->comment_count = $record->comment_count;
1285      }
1286    }
1287  }
1288  
1289  /**
1290   * Implements hook_node_prepare().
1291   */
1292  function comment_node_prepare($node) {
1293    if (!isset($node->comment)) {
1294      $node->comment = variable_get("comment_$node->type", COMMENT_NODE_OPEN);
1295    }
1296  }
1297  
1298  /**
1299   * Implements hook_node_insert().
1300   */
1301  function comment_node_insert($node) {
1302    // Allow bulk updates and inserts to temporarily disable the
1303    // maintenance of the {node_comment_statistics} table.
1304    if (variable_get('comment_maintain_node_statistics', TRUE)) {
1305      db_insert('node_comment_statistics')
1306        ->fields(array(
1307          'nid' => $node->nid,
1308          'cid' => 0,
1309          'last_comment_timestamp' => $node->changed,
1310          'last_comment_name' => NULL,
1311          'last_comment_uid' => $node->uid,
1312          'comment_count' => 0,
1313        ))
1314        ->execute();
1315    }
1316  }
1317  
1318  /**
1319   * Implements hook_node_delete().
1320   */
1321  function comment_node_delete($node) {
1322    $cids = db_query('SELECT cid FROM {comment} WHERE nid = :nid', array(':nid' => $node->nid))->fetchCol();
1323    comment_delete_multiple($cids);
1324    db_delete('node_comment_statistics')
1325      ->condition('nid', $node->nid)
1326      ->execute();
1327  }
1328  
1329  /**
1330   * Implements hook_node_update_index().
1331   */
1332  function comment_node_update_index($node) {
1333    $index_comments = &drupal_static(__FUNCTION__);
1334  
1335    if ($index_comments === NULL) {
1336      // Find and save roles that can 'access comments' or 'search content'.
1337      $perms = array('access comments' => array(), 'search content' => array());
1338      $result = db_query("SELECT rid, permission FROM {role_permission} WHERE permission IN ('access comments', 'search content')");
1339      foreach ($result as $record) {
1340        $perms[$record->permission][$record->rid] = $record->rid;
1341      }
1342  
1343      // Prevent indexing of comments if there are any roles that can search but
1344      // not view comments.
1345      $index_comments = TRUE;
1346      foreach ($perms['search content'] as $rid) {
1347        if (!isset($perms['access comments'][$rid]) && ($rid <= DRUPAL_AUTHENTICATED_RID || !isset($perms['access comments'][DRUPAL_AUTHENTICATED_RID]))) {
1348          $index_comments = FALSE;
1349          break;
1350        }
1351      }
1352    }
1353  
1354    if ($index_comments) {
1355      $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
1356      $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
1357      if ($node->comment && $cids = comment_get_thread($node, $mode, $comments_per_page)) {
1358        $comments = comment_load_multiple($cids);
1359        comment_prepare_thread($comments);
1360        $build = comment_view_multiple($comments, $node);
1361        return drupal_render($build);
1362      }
1363    }
1364    return '';
1365  }
1366  
1367  /**
1368   * Implements hook_update_index().
1369   */
1370  function comment_update_index() {
1371    // Store the maximum possible comments per thread (used for ranking by reply count)
1372    variable_set('node_cron_comments_scale', 1.0 / max(1, db_query('SELECT MAX(comment_count) FROM {node_comment_statistics}')->fetchField()));
1373  }
1374  
1375  /**
1376   * Implements hook_node_search_result().
1377   *
1378   * Formats a comment count string and returns it, for display with search
1379   * results.
1380   */
1381  function comment_node_search_result($node) {
1382    // Do not make a string if comments are hidden.
1383    if (user_access('access comments') && $node->comment != COMMENT_NODE_HIDDEN) {
1384      $comments = db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array('nid' => $node->nid))->fetchField();
1385      // Do not make a string if comments are closed and there are currently
1386      // zero comments.
1387      if ($node->comment != COMMENT_NODE_CLOSED || $comments > 0) {
1388        return array('comment' => format_plural($comments, '1 comment', '@count comments'));
1389      }
1390    }
1391  }
1392  
1393  /**
1394   * Implements hook_user_cancel().
1395   */
1396  function comment_user_cancel($edit, $account, $method) {
1397    switch ($method) {
1398      case 'user_cancel_block_unpublish':
1399        $comments = comment_load_multiple(array(), array('uid' => $account->uid));
1400        foreach ($comments as $comment) {
1401          $comment->status = 0;
1402          comment_save($comment);
1403        }
1404        break;
1405  
1406      case 'user_cancel_reassign':
1407        $comments = comment_load_multiple(array(), array('uid' => $account->uid));
1408        foreach ($comments as $comment) {
1409          $comment->uid = 0;
1410          comment_save($comment);
1411        }
1412        break;
1413    }
1414  }
1415  
1416  /**
1417   * Implements hook_user_delete().
1418   */
1419  function comment_user_delete($account) {
1420    $cids = db_query('SELECT c.cid FROM {comment} c WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol();
1421    comment_delete_multiple($cids);
1422  }
1423  
1424  /**
1425   * Determines whether the current user has access to a particular comment.
1426   *
1427   * Authenticated users can edit their comments as long they have not been
1428   * replied to. This prevents people from changing or revising their statements
1429   * based on the replies to their posts.
1430   *
1431   * @param $op
1432   *   The operation that is to be performed on the comment. Only 'edit' is
1433   *   recognized now.
1434   * @param $comment
1435   *   The comment object.
1436   * @return
1437   *   TRUE if the current user has acces to the comment, FALSE otherwise.
1438   */
1439  function comment_access($op, $comment) {
1440    global $user;
1441  
1442    if ($op == 'edit') {
1443      return ($user->uid && $user->uid == $comment->uid && $comment->status == COMMENT_PUBLISHED && user_access('edit own comments')) || user_access('administer comments');
1444    }
1445  }
1446  
1447  /**
1448   * Accepts a submission of new or changed comment content.
1449   *
1450   * @param $comment
1451   *   A comment object.
1452   */
1453  function comment_save($comment) {
1454    global $user;
1455  
1456    $transaction = db_transaction();
1457    try {
1458      $defaults = array(
1459        'mail' => '',
1460        'homepage' => '',
1461        'name' => '',
1462        'status' => user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED,
1463      );
1464      foreach ($defaults as $key => $default) {
1465        if (!isset($comment->$key)) {
1466          $comment->$key = $default;
1467        }
1468      }
1469      // Make sure we have a bundle name.
1470      if (!isset($comment->node_type)) {
1471        $node = node_load($comment->nid);
1472        $comment->node_type = 'comment_node_' . $node->type;
1473      }
1474  
1475      // Load the stored entity, if any.
1476      if (!empty($comment->cid) && !isset($comment->original)) {
1477        $comment->original = entity_load_unchanged('comment', $comment->cid);
1478      }
1479  
1480      field_attach_presave('comment', $comment);
1481  
1482      // Allow modules to alter the comment before saving.
1483      module_invoke_all('comment_presave', $comment);
1484      module_invoke_all('entity_presave', $comment, 'comment');
1485  
1486      if ($comment->cid) {
1487  
1488        drupal_write_record('comment', $comment, 'cid');
1489  
1490        // Ignore slave server temporarily to give time for the
1491        // saved comment to be propagated to the slave.
1492        db_ignore_slave();
1493  
1494        // Update the {node_comment_statistics} table prior to executing hooks.
1495        _comment_update_node_statistics($comment->nid);
1496  
1497        field_attach_update('comment', $comment);
1498        // Allow modules to respond to the updating of a comment.
1499        module_invoke_all('comment_update', $comment);
1500        module_invoke_all('entity_update', $comment, 'comment');
1501      }
1502      else {
1503        // Add the comment to database. This next section builds the thread field.
1504        // Also see the documentation for comment_view().
1505        if (!empty($comment->thread)) {
1506          // Allow calling code to set thread itself.
1507          $thread = $comment->thread;
1508        }
1509        elseif ($comment->pid == 0) {
1510          // This is a comment with no parent comment (depth 0): we start
1511          // by retrieving the maximum thread level.
1512          $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField();
1513          // Strip the "/" from the end of the thread.
1514          $max = rtrim($max, '/');
1515          // We need to get the value at the correct depth.
1516          $parts = explode('.', $max);
1517          $firstsegment = $parts[0];
1518          // Finally, build the thread field for this new comment.
1519          $thread = int2vancode(vancode2int($firstsegment) + 1) . '/';
1520        }
1521        else {
1522          // This is a comment with a parent comment, so increase the part of the
1523          // thread value at the proper depth.
1524  
1525          // Get the parent comment:
1526          $parent = comment_load($comment->pid);
1527          // Strip the "/" from the end of the parent thread.
1528          $parent->thread = (string) rtrim((string) $parent->thread, '/');
1529          // Get the max value in *this* thread.
1530          $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array(
1531            ':thread' => $parent->thread . '.%',
1532            ':nid' => $comment->nid,
1533          ))->fetchField();
1534  
1535          if ($max == '') {
1536            // First child of this parent.
1537            $thread = $parent->thread . '.' . int2vancode(0) . '/';
1538          }
1539          else {
1540            // Strip the "/" at the end of the thread.
1541            $max = rtrim($max, '/');
1542            // Get the value at the correct depth.
1543            $parts = explode('.', $max);
1544            $parent_depth = count(explode('.', $parent->thread));
1545            $last = $parts[$parent_depth];
1546            // Finally, build the thread field for this new comment.
1547            $thread = $parent->thread . '.' . int2vancode(vancode2int($last) + 1) . '/';
1548          }
1549        }
1550  
1551        if (empty($comment->created)) {
1552          $comment->created = REQUEST_TIME;
1553        }
1554  
1555        if (empty($comment->changed)) {
1556          $comment->changed = $comment->created;
1557        }
1558  
1559        if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well.
1560          $comment->name = $user->name;
1561        }
1562  
1563        // Ensure the parent id (pid) has a value set.
1564        if (empty($comment->pid)) {
1565          $comment->pid = 0;
1566        }
1567  
1568        // Add the values which aren't passed into the function.
1569        $comment->thread = $thread;
1570        $comment->hostname = ip_address();
1571  
1572        drupal_write_record('comment', $comment);
1573  
1574        // Ignore slave server temporarily to give time for the
1575        // created comment to be propagated to the slave.
1576        db_ignore_slave();
1577  
1578        // Update the {node_comment_statistics} table prior to executing hooks.
1579        _comment_update_node_statistics($comment->nid);
1580  
1581        field_attach_insert('comment', $comment);
1582  
1583        // Tell the other modules a new comment has been submitted.
1584        module_invoke_all('comment_insert', $comment);
1585        module_invoke_all('entity_insert', $comment, 'comment');
1586      }
1587      if ($comment->status == COMMENT_PUBLISHED) {
1588        module_invoke_all('comment_publish', $comment);
1589      }
1590      unset($comment->original);
1591    }
1592    catch (Exception $e) {
1593      $transaction->rollback('comment');
1594      watchdog_exception('comment', $e);
1595      throw $e;
1596    }
1597  
1598  }
1599  
1600  /**
1601   * Delete a comment and all its replies.
1602   *
1603   * @param $cid
1604   *   The comment to delete.
1605   */
1606  function comment_delete($cid) {
1607    comment_delete_multiple(array($cid));
1608  }
1609  
1610  /**
1611   * Delete comments and all their replies.
1612   *
1613   * @param $cids
1614   *   The comment to delete.
1615   */
1616  function comment_delete_multiple($cids) {
1617    $comments = comment_load_multiple($cids);
1618    if ($comments) {
1619      $transaction = db_transaction();
1620      try {
1621        // Delete the comments.
1622        db_delete('comment')
1623          ->condition('cid', array_keys($comments), 'IN')
1624          ->execute();
1625        foreach ($comments as $comment) {
1626          field_attach_delete('comment', $comment);
1627          module_invoke_all('comment_delete', $comment);
1628          module_invoke_all('entity_delete', $comment, 'comment');
1629  
1630          // Delete the comment's replies.
1631          $child_cids = db_query('SELECT cid FROM {comment} WHERE pid = :cid', array(':cid' => $comment->cid))->fetchCol();
1632          comment_delete_multiple($child_cids);
1633          _comment_update_node_statistics($comment->nid);
1634        }
1635      }
1636      catch (Exception $e) {
1637        $transaction->rollback();
1638        watchdog_exception('comment', $e);
1639        throw $e;
1640      }
1641    }
1642  }
1643  
1644  /**
1645   * Load comments from the database.
1646   *
1647   * @param $cids
1648   *   An array of comment IDs.
1649   * @param $conditions
1650   *   (deprecated) An associative array of conditions on the {comments}
1651   *   table, where the keys are the database fields and the values are the
1652   *   values those fields must have. Instead, it is preferable to use
1653   *   EntityFieldQuery to retrieve a list of entity IDs loadable by
1654   *   this function.
1655   * @param $reset
1656   *   Whether to reset the internal static entity cache. Note that the static
1657   *   cache is disabled in comment_entity_info() by default.
1658   *
1659   * @return
1660   *   An array of comment objects, indexed by comment ID.
1661   *
1662   * @see entity_load()
1663   * @see EntityFieldQuery
1664   *
1665   * @todo Remove $conditions in Drupal 8.
1666   */
1667  function comment_load_multiple($cids = array(), $conditions = array(), $reset = FALSE) {
1668    return entity_load('comment', $cids, $conditions, $reset);
1669  }
1670  
1671  /**
1672   * Load the entire comment by cid.
1673   *
1674   * @param $cid
1675   *   The identifying comment id.
1676   * @param $reset
1677   *   Whether to reset the internal static entity cache. Note that the static
1678   *   cache is disabled in comment_entity_info() by default.
1679   *
1680   * @return
1681   *   The comment object.
1682   */
1683  function comment_load($cid, $reset = FALSE) {
1684    $comment = comment_load_multiple(array($cid), array(), $reset);
1685    return $comment ? $comment[$cid] : FALSE;
1686  }
1687  
1688  /**
1689   * Controller class for comments.
1690   *
1691   * This extends the DrupalDefaultEntityController class, adding required
1692   * special handling for comment objects.
1693   */
1694  class CommentController extends DrupalDefaultEntityController {
1695  
1696    protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
1697      $query = parent::buildQuery($ids, $conditions, $revision_id);
1698      // Specify additional fields from the user and node tables.
1699      $query->innerJoin('node', 'n', 'base.nid = n.nid');
1700      $query->addField('n', 'type', 'node_type');
1701      $query->innerJoin('users', 'u', 'base.uid = u.uid');
1702      $query->addField('u', 'name', 'registered_name');
1703      $query->fields('u', array('uid', 'signature', 'signature_format', 'picture'));
1704      return $query;
1705    }
1706  
1707    protected function attachLoad(&$comments, $revision_id = FALSE) {
1708      // Setup standard comment properties.
1709      foreach ($comments as $key => $comment) {
1710        $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
1711        $comment->new = node_mark($comment->nid, $comment->changed);
1712        $comment->node_type = 'comment_node_' . $comment->node_type;
1713        $comments[$key] = $comment;
1714      }
1715      parent::attachLoad($comments, $revision_id);
1716    }
1717  }
1718  
1719  /**
1720   * Get number of new comments for current user and specified node.
1721   *
1722   * @param $nid
1723   *   Node-id to count comments for.
1724   * @param $timestamp
1725   *   Time to count from (defaults to time of last user access
1726   *   to node).
1727   * @return The result or FALSE on error.
1728   */
1729  function comment_num_new($nid, $timestamp = 0) {
1730    global $user;
1731  
1732    if ($user->uid) {
1733      // Retrieve the timestamp at which the current user last viewed this node.
1734      if (!$timestamp) {
1735        $timestamp = node_last_viewed($nid);
1736      }
1737      $timestamp = ($timestamp > NODE_NEW_LIMIT ? $timestamp : NODE_NEW_LIMIT);
1738  
1739      // Use the timestamp to retrieve the number of new comments.
1740      return db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND created > :timestamp AND status = :status', array(
1741        ':nid' => $nid,
1742        ':timestamp' => $timestamp,
1743        ':status' => COMMENT_PUBLISHED,
1744        ))->fetchField();
1745    }
1746    else {
1747      return FALSE;
1748    }
1749  
1750  }
1751  
1752  /**
1753   * Get the display ordinal for a comment, starting from 0.
1754   *
1755   * Count the number of comments which appear before the comment we want to
1756   * display, taking into account display settings and threading.
1757   *
1758   * @param $cid
1759   *   The comment ID.
1760   * @param $node_type
1761   *   The node type of the comment's parent.
1762   * @return
1763   *   The display ordinal for the comment.
1764   * @see comment_get_display_page()
1765   */
1766  function comment_get_display_ordinal($cid, $node_type) {
1767    // Count how many comments (c1) are before $cid (c2) in display order. This is
1768    // the 0-based display ordinal.
1769    $query = db_select('comment', 'c1');
1770    $query->innerJoin('comment', 'c2', 'c2.nid = c1.nid');
1771    $query->addExpression('COUNT(*)', 'count');
1772    $query->condition('c2.cid', $cid);
1773    if (!user_access('administer comments')) {
1774      $query->condition('c1.status', COMMENT_PUBLISHED);
1775    }
1776    $mode = variable_get('comment_default_mode_' . $node_type, COMMENT_MODE_THREADED);
1777  
1778    if ($mode == COMMENT_MODE_FLAT) {
1779      // For flat comments, cid is used for ordering comments due to
1780      // unpredicatable behavior with timestamp, so we make the same assumption
1781      // here.
1782      $query->condition('c1.cid', $cid, '<');
1783    }
1784    else {
1785      // For threaded comments, the c.thread column is used for ordering. We can
1786      // use the vancode for comparison, but must remove the trailing slash.
1787      // See comment_view_multiple().
1788      $query->where('SUBSTRING(c1.thread, 1, (LENGTH(c1.thread) -1)) < SUBSTRING(c2.thread, 1, (LENGTH(c2.thread) -1))');
1789    }
1790  
1791    return $query->execute()->fetchField();
1792  }
1793  
1794  /**
1795   * Return the page number for a comment.
1796   *
1797   * Finds the correct page number for a comment taking into account display
1798   * and paging settings.
1799   *
1800   * @param $cid
1801   *   The comment ID.
1802   * @param $node_type
1803   *   The node type the comment is attached to.
1804   * @return
1805   *   The page number.
1806   */
1807  function comment_get_display_page($cid, $node_type) {
1808    $ordinal = comment_get_display_ordinal($cid, $node_type);
1809    $comments_per_page = variable_get('comment_default_per_page_' . $node_type, 50);
1810    return floor($ordinal / $comments_per_page);
1811  }
1812  
1813  /**
1814   * Page callback for comment editing.
1815   */
1816  function comment_edit_page($comment) {
1817    drupal_set_title(t('Edit comment %comment', array('%comment' => $comment->subject)), PASS_THROUGH);
1818    $node = node_load($comment->nid);
1819    return drupal_get_form("comment_node_{$node->type}_form", $comment);
1820  }
1821  
1822  /**
1823   * Implements hook_forms().
1824   */
1825  function comment_forms() {
1826    $forms = array();
1827    foreach (node_type_get_types() as $type) {
1828      $forms["comment_node_{$type->type}_form"]['callback'] = 'comment_form';
1829    }
1830    return $forms;
1831  }
1832  
1833  /**
1834   * Generate the basic commenting form, for appending to a node or display on a separate page.
1835   *
1836   * @see comment_form_validate()
1837   * @see comment_form_submit()
1838   *
1839   * @ingroup forms
1840   */
1841  function comment_form($form, &$form_state, $comment) {
1842    global $user;
1843  
1844    // During initial form build, add the comment entity to the form state for
1845    // use during form building and processing. During a rebuild, use what is in
1846    // the form state.
1847    if (!isset($form_state['comment'])) {
1848      $defaults = array(
1849        'name' => '',
1850        'mail' => '',
1851        'homepage' => '',
1852        'subject' => '',
1853        'comment' => '',
1854        'cid' => NULL,
1855        'pid' => NULL,
1856        'language' => LANGUAGE_NONE,
1857        'uid' => 0,
1858      );
1859      foreach ($defaults as $key => $value) {
1860        if (!isset($comment->$key)) {
1861          $comment->$key = $value;
1862        }
1863      }
1864      $form_state['comment'] = $comment;
1865    }
1866    else {
1867      $comment = $form_state['comment'];
1868    }
1869  
1870    $node = node_load($comment->nid);
1871    $form['#node'] = $node;
1872  
1873    // Use #comment-form as unique jump target, regardless of node type.
1874    $form['#id'] = drupal_html_id('comment_form');
1875    $form['#attributes']['class'][] = 'comment-form';
1876    $form['#theme'] = array('comment_form__node_' . $node->type, 'comment_form');
1877  
1878    $anonymous_contact = variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT);
1879    $is_admin = (!empty($comment->cid) && user_access('administer comments'));
1880  
1881    if (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
1882      $form['#attached']['library'][] = array('system', 'jquery.cookie');
1883      $form['#attributes']['class'][] = 'user-info-from-cookie';
1884    }
1885  
1886    // If not replying to a comment, use our dedicated page callback for new
1887    // comments on nodes.
1888    if (empty($comment->cid) && empty($comment->pid)) {
1889      $form['#action'] = url('comment/reply/' . $comment->nid);
1890    }
1891  
1892    if (isset($form_state['comment_preview'])) {
1893      $form += $form_state['comment_preview'];
1894    }
1895  
1896    // Display author information in a fieldset for comment moderators.
1897    if ($is_admin) {
1898      $form['author'] = array(
1899        '#type' => 'fieldset',
1900        '#title' => t('Administration'),
1901        '#collapsible' => TRUE,
1902        '#collapsed' => TRUE,
1903        '#weight' => -2,
1904      );
1905    }
1906    else {
1907      // Sets the author form elements above the subject.
1908      $form['author'] = array(
1909        '#weight' => -2,
1910      );
1911    }
1912  
1913    // Prepare default values for form elements.
1914    if ($is_admin) {
1915      $author = (!$comment->uid && $comment->name ? $comment->name : $comment->registered_name);
1916      $status = (isset($comment->status) ? $comment->status : COMMENT_NOT_PUBLISHED);
1917      $date = (!empty($comment->date) ? $comment->date : format_date($comment->created, 'custom', 'Y-m-d H:i O'));
1918    }
1919    else {
1920      if ($user->uid) {
1921        $author = $user->name;
1922      }
1923      else {
1924        $author = ($comment->name ? $comment->name : '');
1925      }
1926      $status = (user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED);
1927      $date = '';
1928    }
1929  
1930    // Add the author name field depending on the current user.
1931    if ($is_admin) {
1932      $form['author']['name'] = array(
1933        '#type' => 'textfield',
1934        '#title' => t('Authored by'),
1935        '#default_value' => $author,
1936        '#maxlength' => 60,
1937        '#size' => 30,
1938        '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
1939        '#autocomplete_path' => 'user/autocomplete',
1940      );
1941    }
1942    elseif ($user->uid) {
1943      $form['author']['_author'] = array(
1944        '#type' => 'item',
1945        '#title' => t('Your name'),
1946        '#markup' => theme('username', array('account' => $user)),
1947      );
1948      $form['author']['name'] = array(
1949        '#type' => 'value',
1950        '#value' => $author,
1951      );
1952    }
1953    else {
1954      $form['author']['name'] = array(
1955        '#type' => 'textfield',
1956        '#title' => t('Your name'),
1957        '#default_value' => $author,
1958        '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
1959        '#maxlength' => 60,
1960        '#size' => 30,
1961      );
1962    }
1963  
1964    // Add author e-mail and homepage fields depending on the current user.
1965    $form['author']['mail'] = array(
1966      '#type' => 'textfield',
1967      '#title' => t('E-mail'),
1968      '#default_value' => $comment->mail,
1969      '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
1970      '#maxlength' => 64,
1971      '#size' => 30,
1972      '#description' => t('The content of this field is kept private and will not be shown publicly.'),
1973      '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
1974    );
1975    $form['author']['homepage'] = array(
1976      '#type' => 'textfield',
1977      '#title' => t('Homepage'),
1978      '#default_value' => $comment->homepage,
1979      '#maxlength' => 255,
1980      '#size' => 30,
1981      '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
1982    );
1983  
1984    // Add administrative comment publishing options.
1985    $form['author']['date'] = array(
1986      '#type' => 'textfield',
1987      '#title' => t('Authored on'),
1988      '#default_value' => $date,
1989      '#maxlength' => 25,
1990      '#size' => 20,
1991      '#access' => $is_admin,
1992    );
1993    $form['author']['status'] = array(
1994      '#type' => 'radios',
1995      '#title' => t('Status'),
1996      '#default_value' => $status,
1997      '#options' => array(
1998        COMMENT_PUBLISHED => t('Published'),
1999        COMMENT_NOT_PUBLISHED => t('Not published'),
2000      ),
2001      '#access' => $is_admin,
2002    );
2003  
2004    $form['subject'] = array(
2005      '#type' => 'textfield',
2006      '#title' => t('Subject'),
2007      '#maxlength' => 64,
2008      '#default_value' => $comment->subject,
2009      '#access' => variable_get('comment_subject_field_' . $node->type, 1) == 1,
2010      '#weight' => -1,
2011    );
2012  
2013    // Used for conditional validation of author fields.
2014    $form['is_anonymous'] = array(
2015      '#type' => 'value',
2016      '#value' => ($comment->cid ? !$comment->uid : !$user->uid),
2017    );
2018  
2019    // Add internal comment properties.
2020    foreach (array('cid', 'pid', 'nid', 'language', 'uid') as $key) {
2021      $form[$key] = array('#type' => 'value', '#value' => $comment->$key);
2022    }
2023    $form['node_type'] = array('#type' => 'value', '#value' => 'comment_node_' . $node->type);
2024  
2025    // Only show the save button if comment previews are optional or if we are
2026    // already previewing the submission.
2027    $form['actions'] = array('#type' => 'actions');
2028    $form['actions']['submit'] = array(
2029      '#type' => 'submit',
2030      '#value' => t('Save'),
2031      '#access' => ($comment->cid && user_access('administer comments')) || variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || isset($form_state['comment_preview']),
2032      '#weight' => 19,
2033    );
2034    $form['actions']['preview'] = array(
2035      '#type' => 'submit',
2036      '#value' => t('Preview'),
2037      '#access' => (variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_DISABLED),
2038      '#weight' => 20,
2039      '#submit' => array('comment_form_build_preview'),
2040    );
2041  
2042    // Attach fields.
2043    $comment->node_type = 'comment_node_' . $node->type;
2044    field_attach_form('comment', $comment, $form, $form_state);
2045  
2046    return $form;
2047  }
2048  
2049  /**
2050   * Build a preview from submitted form values.
2051   */
2052  function comment_form_build_preview($form, &$form_state) {
2053    $comment = comment_form_submit_build_comment($form, $form_state);
2054    $form_state['comment_preview'] = comment_preview($comment);
2055    $form_state['rebuild'] = TRUE;
2056  }
2057  
2058  /**
2059   * Generate a comment preview.
2060   */
2061  function comment_preview($comment) {
2062    global $user;
2063  
2064    drupal_set_title(t('Preview comment'), PASS_THROUGH);
2065  
2066    $node = node_load($comment->nid);
2067  
2068    if (!form_get_errors()) {
2069      $comment->format = $comment->comment_body[LANGUAGE_NONE][0]['format'];
2070      // Attach the user and time information.
2071      if (!empty($comment->name)) {
2072        $account = user_load_by_name($comment->name);
2073      }
2074      elseif ($user->uid && empty($comment->is_anonymous)) {
2075        $account = $user;
2076      }
2077  
2078      if (!empty($account->uid)) {
2079        $comment->uid = $account->uid;
2080        $comment->name = check_plain($account->name);
2081        $comment->signature = $account->signature;
2082        $comment->signature_format = $account->signature_format;
2083        $comment->picture = $account->picture;
2084      }
2085      elseif (empty($comment->name)) {
2086        $comment->name = variable_get('anonymous', t('Anonymous'));
2087      }
2088  
2089      $comment->created = !empty($comment->created) ? $comment->created : REQUEST_TIME;
2090      $comment->changed = REQUEST_TIME;
2091      $comment->in_preview = TRUE;
2092      $comment_build = comment_view($comment, $node);
2093      $comment_build['#weight'] = -100;
2094  
2095      $form['comment_preview'] = $comment_build;
2096    }
2097  
2098    if ($comment->pid) {
2099      $build = array();
2100      if ($comments = comment_load_multiple(array($comment->pid), array('status' => COMMENT_PUBLISHED))) {
2101        $parent_comment = $comments[$comment->pid];
2102        $build = comment_view($parent_comment, $node);
2103      }
2104    }
2105    else {
2106      $build = node_view($node);
2107    }
2108  
2109    $form['comment_output_below'] = $build;
2110    $form['comment_output_below']['#weight'] = 100;
2111  
2112    return $form;
2113  }
2114  
2115  /**
2116   * Validate comment form submissions.
2117   */
2118  function comment_form_validate($form, &$form_state) {
2119    global $user;
2120  
2121    entity_form_field_validate('comment', $form, $form_state);
2122  
2123    if (!empty($form_state['values']['cid'])) {
2124      // Verify the name in case it is being changed from being anonymous.
2125      $account = user_load_by_name($form_state['values']['name']);
2126      $form_state['values']['uid'] = $account ? $account->uid : 0;
2127  
2128      if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) {
2129        form_set_error('date', t('You have to specify a valid date.'));
2130      }
2131      if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) {
2132        form_set_error('name', t('You have to specify a valid author.'));
2133      }
2134    }
2135    elseif ($form_state['values']['is_anonymous']) {
2136      // Validate anonymous comment author fields (if given). If the (original)
2137      // author of this comment was an anonymous user, verify that no registered
2138      // user with this name exists.
2139      if ($form_state['values']['name']) {
2140        $query = db_select('users', 'u');
2141        $query->addField('u', 'uid', 'uid');
2142        $taken = $query
2143          ->condition('name', db_like($form_state['values']['name']), 'LIKE')
2144          ->countQuery()
2145          ->execute()
2146          ->fetchField();
2147        if ($taken) {
2148          form_set_error('name', t('The name you used belongs to a registered user.'));
2149        }
2150      }
2151    }
2152    if ($form_state['values']['mail'] && !valid_email_address($form_state['values']['mail'])) {
2153      form_set_error('mail', t('The e-mail address you specified is not valid.'));
2154    }
2155    if ($form_state['values']['homepage'] && !valid_url($form_state['values']['homepage'], TRUE)) {
2156      form_set_error('homepage', t('The URL of your homepage is not valid. Remember that it must be fully qualified, i.e. of the form <code>http://example.com/directory</code>.'));
2157    }
2158  }
2159  
2160  /**
2161   * Prepare a comment for submission.
2162   */
2163  function comment_submit($comment) {
2164    // @todo Legacy support. Remove in Drupal 8.
2165    if (is_array($comment)) {
2166      $comment += array('subject' => '');
2167      $comment = (object) $comment;
2168    }
2169  
2170    if (empty($comment->date)) {
2171      $comment->date = 'now';
2172    }
2173    $comment->created = strtotime($comment->date);
2174    $comment->changed = REQUEST_TIME;
2175  
2176    // If the comment was posted by a registered user, assign the author's ID.
2177    // @todo Too fragile. Should be prepared and stored in comment_form() already.
2178    if (!$comment->is_anonymous && !empty($comment->name) && ($account = user_load_by_name($comment->name))) {
2179      $comment->uid = $account->uid;
2180    }
2181    // If the comment was posted by an anonymous user and no author name was
2182    // required, use "Anonymous" by default.
2183    if ($comment->is_anonymous && (!isset($comment->name) || $comment->name === '')) {
2184      $comment->name = variable_get('anonymous', t('Anonymous'));
2185    }
2186  
2187    // Validate the comment's subject. If not specified, extract from comment body.
2188    if (trim($comment->subject) == '') {
2189      // The body may be in any format, so:
2190      // 1) Filter it into HTML
2191      // 2) Strip out all HTML tags
2192      // 3) Convert entities back to plain-text.
2193      $comment_body = $comment->comment_body[LANGUAGE_NONE][0];
2194      if (isset($comment_body['format'])) {
2195        $comment_text = check_markup($comment_body['value'], $comment_body['format']);
2196      }
2197      else {
2198        $comment_text = check_plain($comment_body['value']);
2199      }
2200      $comment->subject = truncate_utf8(trim(decode_entities(strip_tags($comment_text))), 29, TRUE);
2201      // Edge cases where the comment body is populated only by HTML tags will
2202      // require a default subject.
2203      if ($comment->subject == '') {
2204        $comment->subject = t('(No subject)');
2205      }
2206    }
2207    return $comment;
2208  }
2209  
2210  /**
2211   * Updates the form state's comment entity by processing this submission's values.
2212   *
2213   * This is the default builder function for the comment form. It is called
2214   * during the "Save" and "Preview" submit handlers to retrieve the entity to
2215   * save or preview. This function can also be called by a "Next" button of a
2216   * wizard to update the form state's entity with the current step's values
2217   * before proceeding to the next step.
2218   *
2219   * @see comment_form()
2220   */
2221  function comment_form_submit_build_comment($form, &$form_state) {
2222    $comment = $form_state['comment'];
2223    entity_form_submit_build_entity('comment', $comment, $form, $form_state);
2224    comment_submit($comment);
2225    return $comment;
2226  }
2227  
2228  /**
2229   * Process comment form submissions; prepare the comment, store it, and set a redirection target.
2230   */
2231  function comment_form_submit($form, &$form_state) {
2232    $node = node_load($form_state['values']['nid']);
2233    $comment = comment_form_submit_build_comment($form, $form_state);
2234    if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) {
2235      // Save the anonymous user information to a cookie for reuse.
2236      if (user_is_anonymous()) {
2237        user_cookie_save(array_intersect_key($form_state['values'], array_flip(array('name', 'mail', 'homepage'))));
2238      }
2239  
2240      comment_save($comment);
2241      $form_state['values']['cid'] = $comment->cid;
2242  
2243      // Add an entry to the watchdog log.
2244      watchdog('content', 'Comment posted: %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)));
2245  
2246      // Explain the approval queue if necessary.
2247      if ($comment->status == COMMENT_NOT_PUBLISHED) {
2248        if (!user_access('administer comments')) {
2249          drupal_set_message(t('Your comment has been queued for review by site administrators and will be published after approval.'));
2250        }
2251      }
2252      else {
2253        drupal_set_message(t('Your comment has been posted.'));
2254      }
2255      $query = array();
2256      // Find the current display page for this comment.
2257      $page = comment_get_display_page($comment->cid, $node->type);
2258      if ($page > 0) {
2259        $query['page'] = $page;
2260      }
2261      // Redirect to the newly posted comment.
2262      $redirect = array('node/' . $node->nid, array('query' => $query, 'fragment' => 'comment-' . $comment->cid));
2263    }
2264    else {
2265      watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject), WATCHDOG_WARNING);
2266      drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject)), 'error');
2267      // Redirect the user to the node they are commenting on.
2268      $redirect = 'node/' . $node->nid;
2269    }
2270    $form_state['redirect'] = $redirect;
2271    // Clear the block and page caches so that anonymous users see the comment
2272    // they have posted.
2273    cache_clear_all();
2274  }
2275  
2276  /**
2277   * Process variables for comment.tpl.php.
2278   *
2279   * @see comment.tpl.php
2280   */
2281  function template_preprocess_comment(&$variables) {
2282    $comment = $variables['elements']['#comment'];
2283    $node = $variables['elements']['#node'];
2284    $variables['comment']   = $comment;
2285    $variables['node']      = $node;
2286    $variables['author']    = theme('username', array('account' => $comment));
2287    $variables['created']   = format_date($comment->created);
2288    $variables['changed']   = format_date($comment->changed);
2289  
2290    $variables['new']       = !empty($comment->new) ? t('new') : '';
2291    $variables['picture']   = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', array('account' => $comment)) : '';
2292    $variables['signature'] = $comment->signature;
2293  
2294    $uri = entity_uri('comment', $comment);
2295    $uri['options'] += array('attributes' => array('class' => 'permalink', 'rel' => 'bookmark'));
2296  
2297    $variables['title']     = l($comment->subject, $uri['path'], $uri['options']);
2298    $variables['permalink'] = l(t('Permalink'), $uri['path'], $uri['options']);
2299    $variables['submitted'] = t('Submitted by !username on !datetime', array('!username' => $variables['author'], '!datetime' => $variables['created']));
2300  
2301    // Preprocess fields.
2302    field_attach_preprocess('comment', $comment, $variables['elements'], $variables);
2303  
2304    // Helpful $content variable for templates.
2305    foreach (element_children($variables['elements']) as $key) {
2306      $variables['content'][$key] = $variables['elements'][$key];
2307    }
2308  
2309    // Set status to a string representation of comment->status.
2310    if (isset($comment->in_preview)) {
2311      $variables['status'] = 'comment-preview';
2312    }
2313    else {
2314      $variables['status'] = ($comment->status == COMMENT_NOT_PUBLISHED) ? 'comment-unpublished' : 'comment-published';
2315    }
2316  
2317    // Gather comment classes.
2318    // 'comment-published' class is not needed, it is either 'comment-preview' or
2319    // 'comment-unpublished'.
2320    if ($variables['status'] != 'comment-published') {
2321      $variables['classes_array'][] = $variables['status'];
2322    }
2323    if ($variables['new']) {
2324      $variables['classes_array'][] = 'comment-new';
2325    }
2326    if (!$comment->uid) {
2327      $variables['classes_array'][] = 'comment-by-anonymous';
2328    }
2329    else {
2330      if ($comment->uid == $variables['node']->uid) {
2331        $variables['classes_array'][] = 'comment-by-node-author';
2332      }
2333      if ($comment->uid == $variables['user']->uid) {
2334        $variables['classes_array'][] = 'comment-by-viewer';
2335      }
2336    }
2337  }
2338  
2339  /**
2340   * Returns HTML for a "you can't post comments" notice.
2341   *
2342   * @param $variables
2343   *   An associative array containing:
2344   *   - node: The comment node.
2345   *
2346   * @ingroup themeable
2347   */
2348  function theme_comment_post_forbidden($variables) {
2349    $node = $variables['node'];
2350    global $user;
2351  
2352    // Since this is expensive to compute, we cache it so that a page with many
2353    // comments only has to query the database once for all the links.
2354    $authenticated_post_comments = &drupal_static(__FUNCTION__, NULL);
2355  
2356    if (!$user->uid) {
2357      if (!isset($authenticated_post_comments)) {
2358        // We only output a link if we are certain that users will get permission
2359        // to post comments by logging in.
2360        $comment_roles = user_roles(TRUE, 'post comments');
2361        $authenticated_post_comments = isset($comment_roles[DRUPAL_AUTHENTICATED_RID]);
2362      }
2363  
2364      if ($authenticated_post_comments) {
2365        // We cannot use drupal_get_destination() because these links
2366        // sometimes appear on /node and taxonomy listing pages.
2367        if (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_SEPARATE_PAGE) {
2368          $destination = array('destination' => "comment/reply/$node->nid#comment-form");
2369        }
2370        else {
2371          $destination = array('destination' => "node/$node->nid#comment-form");
2372        }
2373  
2374        if (variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)) {
2375          // Users can register themselves.
2376          return t('<a href="@login">Log in</a> or <a href="@register">register</a> to post comments', array('@login' => url('user/login', array('query' => $destination)), '@register' => url('user/register', array('query' => $destination))));
2377        }
2378        else {
2379          // Only admins can add new users, no public registration.
2380          return t('<a href="@login">Log in</a> to post comments', array('@login' => url('user/login', array('query' => $destination))));
2381        }
2382      }
2383    }
2384  }
2385  
2386  /**
2387   * Process variables for comment-wrapper.tpl.php.
2388   *
2389   * @see comment-wrapper.tpl.php
2390   */
2391  function template_preprocess_comment_wrapper(&$variables) {
2392    // Provide contextual information.
2393    $variables['node'] = $variables['content']['#node'];
2394    $variables['display_mode'] = variable_get('comment_default_mode_' . $variables['node']->type, COMMENT_MODE_THREADED);
2395    // The comment form is optional and may not exist.
2396    $variables['content'] += array('comment_form' => array());
2397  }
2398  
2399  /**
2400   * Return an array of viewing modes for comment listings.
2401   *
2402   * We can't use a global variable array because the locale system
2403   * is not initialized yet when the comment module is loaded.
2404   */
2405  function _comment_get_modes() {
2406    return array(
2407      COMMENT_MODE_FLAT => t('Flat list'),
2408      COMMENT_MODE_THREADED => t('Threaded list')
2409    );
2410  }
2411  
2412  /**
2413   * Return an array of "comments per page" settings from which the user
2414   * can choose.
2415   */
2416  function _comment_per_page() {
2417    return drupal_map_assoc(array(10, 30, 50, 70, 90, 150, 200, 250, 300));
2418  }
2419  
2420  /**
2421   * Updates the comment statistics for a given node. This should be called any
2422   * time a comment is added, deleted, or updated.
2423   *
2424   * The following fields are contained in the node_comment_statistics table.
2425   * - last_comment_timestamp: the timestamp of the last comment for this node or the node create stamp if no comments exist for the node.
2426   * - last_comment_name: the name of the anonymous poster for the last comment
2427   * - last_comment_uid: the uid of the poster for the last comment for this node or the node authors uid if no comments exists for the node.
2428   * - comment_count: the total number of approved/published comments on this node.
2429   */
2430  function _comment_update_node_statistics($nid) {
2431    // Allow bulk updates and inserts to temporarily disable the
2432    // maintenance of the {node_comment_statistics} table.
2433    if (!variable_get('comment_maintain_node_statistics', TRUE)) {
2434      return;
2435    }
2436  
2437    $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array(
2438      ':nid' => $nid,
2439      ':status' => COMMENT_PUBLISHED,
2440    ))->fetchField();
2441  
2442    if ($count > 0) {
2443      // Comments exist.
2444      $last_reply = db_query_range('SELECT cid, name, changed, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', 0, 1, array(
2445        ':nid' => $nid,
2446        ':status' => COMMENT_PUBLISHED,
2447      ))->fetchObject();
2448      db_update('node_comment_statistics')
2449        ->fields(array(
2450          'cid' => $last_reply->cid,
2451          'comment_count' => $count,
2452          'last_comment_timestamp' => $last_reply->changed,
2453          'last_comment_name' => $last_reply->uid ? '' : $last_reply->name,
2454          'last_comment_uid' => $last_reply->uid,
2455        ))
2456        ->condition('nid', $nid)
2457        ->execute();
2458    }
2459    else {
2460      // Comments do not exist.
2461      $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject();
2462      db_update('node_comment_statistics')
2463        ->fields(array(
2464          'cid' => 0,
2465          'comment_count' => 0,
2466          'last_comment_timestamp' => $node->created,
2467          'last_comment_name' => '',
2468          'last_comment_uid' => $node->uid,
2469        ))
2470        ->condition('nid', $nid)
2471        ->execute();
2472    }
2473  }
2474  
2475  /**
2476   * Generate vancode.
2477   *
2478   * Consists of a leading character indicating length, followed by N digits
2479   * with a numerical value in base 36. Vancodes can be sorted as strings
2480   * without messing up numerical order.
2481   *
2482   * It goes:
2483   * 00, 01, 02, ..., 0y, 0z,
2484   * 110, 111, ... , 1zy, 1zz,
2485   * 2100, 2101, ..., 2zzy, 2zzz,
2486   * 31000, 31001, ...
2487   */
2488  function int2vancode($i = 0) {
2489    $num = base_convert((int) $i, 10, 36);
2490    $length = strlen($num);
2491  
2492    return chr($length + ord('0') - 1) . $num;
2493  }
2494  
2495  /**
2496   * Decode vancode back to an integer.
2497   */
2498  function vancode2int($c = '00') {
2499    return base_convert(substr($c, 1), 36, 10);
2500  }
2501  
2502  /**
2503   * Implements hook_action_info().
2504   */
2505  function comment_action_info() {
2506    return array(
2507      'comment_publish_action' => array(
2508        'label' => t('Publish comment'),
2509        'type' => 'comment',
2510        'configurable' => FALSE,
2511        'behavior' => array('changes_property'),
2512        'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
2513      ),
2514      'comment_unpublish_action' => array(
2515        'label' => t('Unpublish comment'),
2516        'type' => 'comment',
2517        'configurable' => FALSE,
2518        'behavior' => array('changes_property'),
2519        'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
2520      ),
2521      'comment_unpublish_by_keyword_action' => array(
2522        'label' => t('Unpublish comment containing keyword(s)'),
2523        'type' => 'comment',
2524        'configurable' => TRUE,
2525        'behavior' => array('changes_property'),
2526        'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
2527      ),
2528      'comment_save_action' => array(
2529        'label' => t('Save comment'),
2530        'type' => 'comment',
2531        'configurable' => FALSE,
2532        'triggers' => array('comment_insert', 'comment_update'),
2533      ),
2534    );
2535  }
2536  
2537  /**
2538   * Publishes a comment.
2539   *
2540   * @param $comment
2541   *   An optional comment object.
2542   * @param array $context
2543   *   Array with components:
2544   *   - 'cid': Comment ID. Required if $comment is not given.
2545   *
2546   * @ingroup actions
2547   */
2548  function comment_publish_action($comment, $context = array()) {
2549    if (isset($comment->subject)) {
2550      $subject = $comment->subject;
2551      $comment->status = COMMENT_PUBLISHED;
2552    }
2553    else {
2554      $cid = $context['cid'];
2555      $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField();
2556      db_update('comment')
2557        ->fields(array('status' => COMMENT_PUBLISHED))
2558        ->condition('cid', $cid)
2559        ->execute();
2560    }
2561    watchdog('action', 'Published comment %subject.', array('%subject' => $subject));
2562  }
2563  
2564  /**
2565   * Unpublishes a comment.
2566   *
2567   * @param $comment
2568   *   An optional comment object.
2569   * @param array $context
2570   *   Array with components:
2571   *   - 'cid': Comment ID. Required if $comment is not given.
2572   *
2573   * @ingroup actions
2574   */
2575  function comment_unpublish_action($comment, $context = array()) {
2576    if (isset($comment->subject)) {
2577      $subject = $comment->subject;
2578      $comment->status = COMMENT_NOT_PUBLISHED;
2579    }
2580    else {
2581      $cid = $context['cid'];
2582      $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField();
2583      db_update('comment')
2584        ->fields(array('status' => COMMENT_NOT_PUBLISHED))
2585        ->condition('cid', $cid)
2586        ->execute();
2587    }
2588    watchdog('action', 'Unpublished comment %subject.', array('%subject' => $subject));
2589  }
2590  
2591  /**
2592   * Unpublishes a comment if it contains certain keywords.
2593   *
2594   * @param $comment
2595   *   Comment object to modify.
2596   * @param array $context
2597   *   Array with components:
2598   *   - 'keywords': Keywords to look for. If the comment contains at least one
2599   *     of the keywords, it is unpublished.
2600   *
2601   * @ingroup actions
2602   * @see comment_unpublish_by_keyword_action_form()
2603   * @see comment_unpublish_by_keyword_action_submit()
2604   */
2605  function comment_unpublish_by_keyword_action($comment, $context) {
2606    foreach ($context['keywords'] as $keyword) {
2607      $text = drupal_render($comment);
2608      if (strpos($text, $keyword) !== FALSE) {
2609        $comment->status = COMMENT_NOT_PUBLISHED;
2610        watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject));
2611        break;
2612      }
2613    }
2614  }
2615  
2616  /**
2617   * Form builder; Prepare a form for blacklisted keywords.
2618   *
2619   * @ingroup forms
2620   * @see comment_unpublish_by_keyword_action()
2621   * @see comment_unpublish_by_keyword_action_submit()
2622   */
2623  function comment_unpublish_by_keyword_action_form($context) {
2624    $form['keywords'] = array(
2625      '#title' => t('Keywords'),
2626      '#type' => 'textarea',
2627      '#description' => t('The comment will be unpublished if it contains any of the phrases above. Use a case-sensitive, comma-separated list of phrases. Example: funny, bungee jumping, "Company, Inc."'),
2628      '#default_value' => isset($context['keywords']) ? drupal_implode_tags($context['keywords']) : '',
2629    );
2630  
2631    return $form;
2632  }
2633  
2634  /**
2635   * Process comment_unpublish_by_keyword_action_form form submissions.
2636   *
2637   * @see comment_unpublish_by_keyword_action()
2638   */
2639  function comment_unpublish_by_keyword_action_submit($form, $form_state) {
2640    return array('keywords' => drupal_explode_tags($form_state['values']['keywords']));
2641  }
2642  
2643  /**
2644   * Saves a comment.
2645   *
2646   * @ingroup actions
2647   */
2648  function comment_save_action($comment) {
2649    comment_save($comment);
2650    cache_clear_all();
2651    watchdog('action', 'Saved comment %title', array('%title' => $comment->subject));
2652  }
2653  
2654  /**
2655   * Implements hook_ranking().
2656   */
2657  function comment_ranking() {
2658    return array(
2659      'comments' => array(
2660        'title' => t('Number of comments'),
2661        'join' => array(
2662          'type' => 'LEFT',
2663          'table' => 'node_comment_statistics',
2664          'alias' => 'node_comment_statistics',
2665          'on' => 'node_comment_statistics.nid = i.sid',
2666        ),
2667        // Inverse law that maps the highest reply count on the site to 1 and 0 to 0.
2668        'score' => '2.0 - 2.0 / (1.0 + node_comment_statistics.comment_count * CAST(:scale AS DECIMAL))',
2669        'arguments' => array(':scale' => variable_get('node_cron_comments_scale', 0)),
2670      ),
2671    );
2672  }
2673  
2674  /**
2675   * Implements hook_rdf_mapping().
2676   */
2677  function comment_rdf_mapping() {
2678    return array(
2679      array(
2680        'type' => 'comment',
2681        'bundle' => RDF_DEFAULT_BUNDLE,
2682        'mapping' => array(
2683          'rdftype' => array('sioc:Post', 'sioct:Comment'),
2684          'title' => array(
2685            'predicates' => array('dc:title'),
2686          ),
2687          'created' => array(
2688            'predicates' => array('dc:date', 'dc:created'),
2689            'datatype' => 'xsd:dateTime',
2690            'callback' => 'date_iso8601',
2691          ),
2692          'changed' => array(
2693            'predicates' => array('dc:modified'),
2694            'datatype' => 'xsd:dateTime',
2695            'callback' => 'date_iso8601',
2696          ),
2697          'comment_body' => array(
2698            'predicates' => array('content:encoded'),
2699          ),
2700          'pid' => array(
2701            'predicates' => array('sioc:reply_of'),
2702            'type' => 'rel',
2703          ),
2704          'uid' => array(
2705            'predicates' => array('sioc:has_creator'),
2706            'type' => 'rel',
2707          ),
2708          'name' => array(
2709            'predicates' => array('foaf:name'),
2710          ),
2711        ),
2712      ),
2713    );
2714  }
2715  
2716  /**
2717   * Implements hook_file_download_access().
2718   */
2719  function comment_file_download_access($field, $entity_type, $entity) {
2720    if ($entity_type == 'comment') {
2721      if (user_access('access comments') && $entity->status == COMMENT_PUBLISHED || user_access('administer comments')) {
2722        $node = node_load($entity->nid);
2723        return node_access('view', $node);
2724      }
2725      return FALSE;
2726    }
2727  }

title

Description

title

Description

title

Description

title

title

Body