comment.module 85.6 KB
Newer Older
1
<?php
2
// $Id$
Dries's avatar
 
Dries committed
3

Dries's avatar
Dries committed
4 5
/**
 * @file
Dries's avatar
 
Dries committed
6
 * Enables users to comment on published content.
Dries's avatar
Dries committed
7 8 9 10 11 12
 *
 * When enabled, the Drupal comment module creates a discussion
 * board for each Drupal node. Users can post comments to discuss
 * a forum topic, weblog post, story, collaborative book page, etc.
 */

13
/**
14
 * Comment is awaiting approval.
15
 */
16
define('COMMENT_NOT_PUBLISHED', 0);
17 18

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

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

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

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

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

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

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

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

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

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

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

73
/**
74
 * Implements hook_help().
75
 */
76 77
function comment_help($path, $arg) {
  switch ($path) {
Dries's avatar
 
Dries committed
78
    case 'admin/help#comment':
79 80 81 82 83
      $output = '<h3>' . t('About') . '</h3>';
      $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/handbook/modules/comment/')) . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Default and custom settings') . '</dt>';
84
      $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>';
85 86 87
      $output .= '<dt>' . t('Comment moderation') . '</dt>';
      $output .= '<dd>' . t("Comments from users who do not have the <em>Post comments without approval</em> permission are placed in the <a href='@comment-approval'>Unapproved comments</a> queue, until a user who has permission to <em>Administer comments</em> moderates them as either published or deleted. 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>';
      $output .= '</dl>';
88
      return $output;
89
  }
Dries's avatar
 
Dries committed
90 91
}

92
/**
93
 * Implements hook_entity_info().
94 95 96 97 98 99
 */
function comment_entity_info() {
  $return =  array(
    'comment' => array(
      'label' => t('Comment'),
      'base table' => 'comment',
100
      'uri callback' => 'comment_uri',
101 102
      'fieldable' => TRUE,
      'controller class' => 'CommentController',
103
      'entity keys' => array(
104 105 106 107
        'id' => 'cid',
        'bundle' => 'node_type',
      ),
      'bundles' => array(),
108 109 110
      'view modes' => array(
        'full' => array(
          'label' => t('Full comment'),
111
          'custom settings' => FALSE,
112 113
        ),
      ),
114 115 116 117 118 119
      'static cache' => FALSE,
    ),
  );

  foreach (node_type_get_names() as $type => $name) {
    $return['comment']['bundles']['comment_node_' . $type] = array(
120
      'label' => t('@node_type comment', array('@node_type' => $name)),
121 122 123 124 125 126
      'admin' => array(
        // Place the Field UI paths for comments one level below the
        // corresponding paths for nodes, so that they appear in the same set
        // of local tasks. Note that the paths use a different placeholder name
        // and thus a different menu loader callback, so that Field UI page
        // callbacks get a comment bundle name from the node type in the URL.
127
        // See comment_node_type_load() and comment_menu_alter().
128 129 130 131 132
        'path' => 'admin/structure/types/manage/%comment_node_type/comment',
        'bundle argument' => 4,
        'real path' => 'admin/structure/types/manage/' . str_replace('_', '-', $type) . '/comment',
        'access arguments' => array('administer content types'),
      ),
133 134 135 136 137 138
    );
  }

  return $return;
}

139 140 141 142 143 144 145 146 147 148 149
/**
 * Menu loader callback for Field UI paths.
 *
 * Return a comment bundle name from a node type in the URL.
 */
function comment_node_type_load($name) {
  if ($type = node_type_get_type(strtr($name, array('-' => '_')))) {
    return 'comment_node_' . $type->type;
  }
}

150
/**
151
 * Entity uri callback.
152
 */
153 154 155 156 157
function comment_uri($comment) {
  return array(
    'path' => 'comment/' . $comment->cid,
    'options' => array('fragment' => 'comment-' . $comment->cid),
  );
158 159
}

160 161 162 163 164 165 166 167 168
/**
 * Implements hook_field_extra_fields().
 */
function comment_field_extra_fields() {
  $return = array();

  foreach (node_type_get_types() as $type) {
    if (variable_get('comment_subject_field_' . $type->type, 1) == 1) {
      $return['comment']['comment_node_' . $type->type] = array(
169 170 171 172 173 174
        'form' => array(
          'author' => array(
            'label' => t('Author'),
            'description' => t('Author textfield'),
            'weight' => -2,
          ),
175
          'subject' => array(
176 177 178 179
            'label' => t('Subject'),
            'description' => t('Subject textfield'),
            'weight' => -1,
          ),
180 181 182 183 184 185 186 187
        ),
      );
    }
  }

  return $return;
}

188
/**
189
 * Implements hook_theme().
190 191 192 193
 */
function comment_theme() {
  return array(
    'comment_block' => array(
194
      'variables' => array(),
195 196
    ),
    'comment_preview' => array(
197
      'variables' => array('comment' => NULL),
198 199
    ),
    'comment' => array(
200
      'template' => 'comment',
201
      'render element' => 'elements',
202 203
    ),
    'comment_post_forbidden' => array(
204
      'variables' => array('node' => NULL),
205 206
    ),
    'comment_wrapper' => array(
207
      'template' => 'comment-wrapper',
208
      'render element' => 'content',
209 210 211 212
    ),
  );
}

Dries's avatar
 
Dries committed
213
/**
214
 * Implements hook_menu().
Dries's avatar
 
Dries committed
215
 */
216
function comment_menu() {
217
  $items['admin/content/comment'] = array(
218
    'title' => 'Comments',
219
    'description' => 'List and edit site comments and the comment approval queue.',
220 221
    'page callback' => 'comment_admin',
    'access arguments' => array('administer comments'),
222
    'type' => MENU_LOCAL_TASK,
223
    'file' => 'comment.admin.inc',
224
  );
225
  // Tabs begin here.
226
  $items['admin/content/comment/new'] = array(
227
    'title' => 'Published comments',
228 229 230
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
231
  $items['admin/content/comment/approval'] = array(
232 233
    'title' => 'Unapproved comments',
    'title callback' => 'comment_count_unpublished',
234
    'page arguments' => array('approval'),
235
    'access arguments' => array('administer comments'),
236 237
    'type' => MENU_LOCAL_TASK,
  );
238
  $items['comment/%'] = array(
239 240 241 242
    'title' => 'Comment permalink',
    'page callback' => 'comment_permalink',
    'page arguments' => array(1),
    'access arguments' => array('access comments'),
243 244
    'type' => MENU_CALLBACK,
  );
245
  $items['comment/%/view'] = array(
246 247 248 249
    'title' => 'View comment',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
250 251
  // Every other comment path uses %, but this one loads the comment directly,
  // so we don't end up loading it twice (in the page and access callback).
252 253
  $items['comment/%comment/edit'] = array(
    'title' => 'Edit',
254 255
    'page callback' => 'comment_edit_page',
    'page arguments' => array(1),
256
    'access callback' => 'comment_access',
257 258 259 260
    'access arguments' => array('edit', 1),
    'type' => MENU_LOCAL_TASK,
    'weight' => 0,
  );
261
  $items['comment/%/approve'] = array(
262 263 264 265
    'title' => 'Approve',
    'page callback' => 'comment_approve',
    'page arguments' => array(1),
    'access arguments' => array('administer comments'),
266
    'type' => MENU_CALLBACK,
267 268 269
    'file' => 'comment.pages.inc',
    'weight' => 1,
  );
270
  $items['comment/%/delete'] = array(
271
    'title' => 'Delete',
272 273
    'page callback' => 'comment_confirm_delete_page',
    'page arguments' => array(1),
274 275 276 277
    'access arguments' => array('administer comments'),
    'type' => MENU_LOCAL_TASK,
    'file' => 'comment.admin.inc',
    'weight' => 2,
278
  );
279
  $items['comment/reply/%node'] = array(
280
    'title' => 'Add new comment',
281
    'page callback' => 'comment_reply',
282
    'page arguments' => array(2),
283 284 285
    'access callback' => 'node_access',
    'access arguments' => array('view', 2),
    'type' => MENU_CALLBACK,
286
    'file' => 'comment.pages.inc',
287
  );
Dries's avatar
 
Dries committed
288 289 290 291

  return $items;
}

292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
/**
 * Implements hook_menu_alter().
 */
function comment_menu_alter(&$items) {
  // Add comments to the description for admin/content.
  $items['admin/content']['description'] = "Administer content and comments";

  // Adjust the Field UI tabs on admin/structure/types/manage/[node-type].
  // See comment_entity_info().
  $items['admin/structure/types/manage/%comment_node_type/comment/fields']['title'] = 'Comment fields';
  $items['admin/structure/types/manage/%comment_node_type/comment/fields']['weight'] = 3;
  $items['admin/structure/types/manage/%comment_node_type/comment/display']['title'] = 'Comment display';
  $items['admin/structure/types/manage/%comment_node_type/comment/display']['weight'] = 4;
}

307 308 309 310 311 312 313 314 315 316
/**
 * Returns a menu title which includes the number of unapproved comments.
 */
function comment_count_unpublished() {
  $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE status = :status', array(
    ':status' => COMMENT_NOT_PUBLISHED,
  ))->fetchField();
  return t('Unapproved comments (@count)', array('@count' => $count));
}

317
/**
318
 * Implements hook_node_type_insert().
319
 */
320
function comment_node_type_insert($info) {
321
  field_attach_create_bundle('comment', 'comment_node_' . $info->type);
322 323 324
  // @todo Create a comment_field_attach_create_bundle() function, and have that
  //   function create the comment body field instance.
  _comment_body_field_instance_create($info);
325
}
326

327
/**
328
 * Implements hook_node_type_update().
329 330 331
 */
function comment_node_type_update($info) {
  if (!empty($info->old_type) && $info->type != $info->old_type) {
332
    field_attach_rename_bundle('comment', 'comment_node_' . $info->old_type, 'comment_node_' . $info->type);
333 334
  }
}
335

336
/**
337
 * Implements hook_node_type_delete().
338 339
 */
function comment_node_type_delete($info) {
340
  field_attach_delete_bundle('comment', 'comment_node_' . $info->type);
341 342 343 344 345 346 347 348 349 350 351
  $settings = array(
    'comment',
    'comment_default_mode',
    'comment_default_per_page',
    'comment_anonymous',
    'comment_subject_field',
    'comment_preview',
    'comment_form_location',
  );
  foreach ($settings as $setting) {
    variable_del($setting . '_' . $info->type);
352 353 354
  }
}

355 356 357 358 359 360 361 362 363
 /**
 * Helper function which creates a comment body field instance for a given node
 * type.
 */
function _comment_body_field_instance_create($info) {
  // Attaches the body field by default.
  $instance = array(
    'field_name' => 'comment_body',
    'label' => 'Comment',
364
    'entity_type' => 'comment',
365 366
    'bundle' => 'comment_node_' . $info->type,
    'settings' => array('text_processing' => 1),
367
    'required' => TRUE,
368
    'display' => array(
369
      'default' => array(
370
        'label' => 'hidden',
371 372
        'type' => 'text_default',
        'weight' => 0,
373 374 375 376 377 378
      ),
    ),
  );
  field_create_instance($instance);
}

Dries's avatar
 
Dries committed
379
/**
380
 * Implements hook_permission().
Dries's avatar
 
Dries committed
381
 */
382
function comment_permission() {
383
  return array(
384
    'administer comments' => array(
385
      'title' => t('Administer comments and comment settings'),
386 387
    ),
    'access comments' => array(
388
      'title' => t('View comments'),
389 390
    ),
    'post comments' => array(
391
      'title' => t('Post comments with approval'),
392 393 394 395
    ),
    'post comments without approval' => array(
      'title' => t('Post comments without approval'),
    ),
396 397 398
    'edit own comments' => array(
      'title' => t('Edit own comments'),
    ),
399
  );
Dries's avatar
 
Dries committed
400 401 402
}

/**
403
 * Implements hook_block_info().
Dries's avatar
 
Dries committed
404
 */
405
function comment_block_info() {
406
  $blocks['recent']['info'] = t('Recent comments');
407

408 409
  return $blocks;
}
410

411
/**
412
 * Implements hook_block_configure().
413 414 415 416 417 418 419 420
 */
function comment_block_configure($delta = '') {
  $form['comment_block_count'] = array(
    '#type' => 'select',
    '#title' => t('Number of recent comments'),
    '#default_value' => variable_get('comment_block_count', 10),
    '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30)),
  );
421

422 423
  return $form;
}
424

425
/**
426
 * Implements hook_block_save().
427 428
 */
function comment_block_save($delta = '', $edit = array()) {
429
  variable_set('comment_block_count', (int) $edit['comment_block_count']);
430
}
431

432
/**
433
 * Implements hook_block_view().
434 435 436 437 438 439 440
 *
 * Generates a block with the most recent comments.
 */
function comment_block_view($delta = '') {
  if (user_access('access comments')) {
    $block['subject'] = t('Recent comments');
    $block['content'] = theme('comment_block');
441

442
    return $block;
Dries's avatar
 
Dries committed
443 444 445
  }
}

446 447 448 449 450 451 452 453 454 455 456
/**
 * Redirects comment links to the correct page depending on comment settings.
 *
 * Since comments are paged there is no way to guarantee which page a comment
 * appears on. Comment paging and threading settings may be changed at any time.
 * With threaded comments, an individual comment may move between pages as
 * comments can be added either before or after it in the overall discussion.
 * Therefore we use a central routing function for comment links, which
 * calculates the page number based on current comment settings and returns
 * the full comment view with the pager set dynamically.
 *
457 458
 * @param $cid
 *   A comment identifier.
459 460 461
 * @return
 *   The comment listing set to the page on which the comment appears.
 */
462 463
function comment_permalink($cid) {
  if (($comment = comment_load($cid)) && ($node = node_load($comment->nid))) {
464 465 466 467 468 469 470 471 472 473

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

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

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

479
/**
480 481 482
 * Find the most recent comments that are available to the current user.
 *
 * @param integer $number
483
 *   (optional) The maximum number of comments to find. Defaults to 10.
484
 * @return
485 486
 *   An array of comment objects or an empty array if there are no recent
 *   comments visible to the current user.
487 488
 */
function comment_get_recent($number = 10) {
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
  $query = db_select('comment', 'c');
  $query->innerJoin('node', 'n', 'n.nid = c.nid');
  $query->innerJoin('node_comment_statistics', 'ncs', 'ncs.nid = c.nid');
  $query->addTag('node_access');
  $comments = $query
    ->fields('c')
    ->condition('ncs.comment_count', 0, '>')
    ->condition('c.status', COMMENT_PUBLISHED)
    ->condition('n.status', NODE_PUBLISHED)
    ->orderBy('ncs.last_comment_timestamp', 'DESC')
    ->orderBy('c.cid', 'DESC')
    ->range(0, $number)
    ->execute()
    ->fetchAll();

  return $comments ? $comments : array();
505 506
}

507 508
/**
 * Calculate page number for first new comment.
509 510 511 512 513 514 515 516 517
 *
 * @param $num_comments
 *   Number of comments.
 * @param $new_replies
 *   Number of new replies.
 * @param $node
 *   The first new comment node.
 * @return
 *   "page=X" if the page number is greater than zero; empty string otherwise.
518
 */
519
function comment_new_page_count($num_comments, $new_replies, $node) {
520 521
  $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
  $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
522
  $pagenum = NULL;
523
  $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
524 525
  if ($num_comments <= $comments_per_page) {
    // Only one page of comments.
526
    $pageno = 0;
527
  }
528 529 530 531 532
  elseif ($flat) {
    // Flat comments.
    $count = $num_comments - $new_replies;
    $pageno =  $count / $comments_per_page;
  }
533
  else {
534 535 536 537 538 539 540 541
    // Threaded comments: we build a query with a subquery to find the first
    // thread with a new comment.

    // 1. Find all the threads with a new comment.
    $unread_threads_query = db_select('comment')
      ->fields('comment', array('thread'))
      ->condition('nid', $node->nid)
      ->condition('status', COMMENT_PUBLISHED)
542 543
      ->orderBy('created', 'DESC')
      ->orderBy('cid', 'DESC')
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559
      ->range(0, $new_replies);

    // 2. Find the first thread.
    $first_thread = db_select($unread_threads_query, 'thread')
      ->fields('thread', array('thread'))
      ->orderBy('SUBSTRING(thread, 1, (LENGTH(thread) - 1))')
      ->range(0, 1)
      ->execute()
      ->fetchField();

    // Remove the final '/'.
    $first_thread = substr($first_thread, 0, -1);

    // Find the number of the first comment of the first unread thread.
    $count = db_query('SELECT COUNT(*) FROM {comment} WHERE nid = :nid AND status = :status AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread', array(
      ':status' => COMMENT_PUBLISHED,
560
      ':nid' => $node->nid,
561
      ':thread' => $first_thread,
562
    ))->fetchField();
563

564 565
    $pageno =  $count / $comments_per_page;
  }
566

567
  if ($pageno >= 1) {
568
    $pagenum = array('page' => intval($pageno));
569
  }
570

571 572 573
  return $pagenum;
}

574
/**
575
 * Returns HTML for a list of recent comments to be displayed in the comment block.
576 577 578
 *
 * @ingroup themeable
 */
579 580
function theme_comment_block() {
  $items = array();
581 582
  $number = variable_get('comment_block_count', 10);
  foreach (comment_get_recent($number) as $comment) {
583
    $items[] = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)) .'<span>'. t('@time ago', array('@time' => format_interval(REQUEST_TIME - $comment->changed))) .'</span>';
584
  }
585

586
  if ($items) {
587
    return theme('item_list', array('items' => $items));
588
  }
589 590 591
  else {
    return t('No comments available.');
  }
592 593
}

Dries's avatar
 
Dries committed
594
/**
595
 * Implements hook_node_view().
Dries's avatar
 
Dries committed
596
 */
597
function comment_node_view($node, $view_mode) {
Dries's avatar
 
Dries committed
598 599
  $links = array();

600
  if ($node->comment) {
601
    if ($view_mode == 'rss') {
602 603 604 605 606 607 608
      if ($node->comment != COMMENT_NODE_HIDDEN) {
        // Add a comments RSS element which is a URL to the comments of this node.
        $node->rss_elements[] = array(
          'key' => 'comments',
          'value' => url('node/' . $node->nid, array('fragment' => 'comments', 'absolute' => TRUE))
        );
      }
609
    }
610
    elseif ($view_mode == 'teaser') {
611 612 613
      // Teaser view: display the number of comments that have been posted,
      // or a link to add new comments if the user has permission, the node
      // is open to new comments, and there currently are none.
Dries's avatar
 
Dries committed
614
      if (user_access('access comments')) {
615
        if (!empty($node->comment_count)) {
616
          $links['comment-comments'] = array(
617
            'title' => format_plural($node->comment_count, '1 comment', '@count comments'),
618 619
            'href' => "node/$node->nid",
            'attributes' => array('title' => t('Jump to the first comment of this posting.')),
620 621
            'fragment' => 'comments',
            'html' => TRUE,
622
          );
623 624

          $new = comment_num_new($node->nid);
625 626
          if (!$new) {
            $links['comment-new-comments'] = array(
627
              'title' => format_plural($new, '1 new comment', '@count new comments'),
628
              'href' => "node/$node->nid",
629
              'query' => comment_new_page_count($node->comment_count, $new, $node),
630
              'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
631 632
              'fragment' => 'new',
              'html' => TRUE,
633
            );
Dries's avatar
 
Dries committed
634 635 636
          }
        }
        else {
637
          if ($node->comment == COMMENT_NODE_OPEN) {
Dries's avatar
 
Dries committed
638
            if (user_access('post comments')) {
639
              $links['comment-add'] = array(
640
                'title' => t('Add new comment'),
641 642
                'href' => "comment/reply/$node->nid",
                'attributes' => array('title' => t('Add a new comment to this page.')),
643 644
                'fragment' => 'comment-form',
                'html' => TRUE,
645
              );
Dries's avatar
 
Dries committed
646 647
            }
            else {
648
              $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node));
Dries's avatar
 
Dries committed
649 650 651 652 653
            }
          }
        }
      }
    }
654 655 656 657 658
    elseif ($view_mode != 'search_index') {
      // Node in other view modes: add a "post comment" link if the user is
      // allowed to post comments and if this node is allowing new comments.
      // But we don't want this link if we're building the node for search
      // indexing.
659
      if ($node->comment == COMMENT_NODE_OPEN) {
Dries's avatar
 
Dries committed
660
        if (user_access('post comments')) {
661
          $links['comment-add'] = array(
662 663 664 665 666
            'title' => t('Add new comment'),
            'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
            'fragment' => 'comment-form',
            'html' => TRUE,
          );
667
          if (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_SEPARATE_PAGE) {
668
            $links['comment-add']['href'] = "comment/reply/$node->nid";
669 670
          }
          else {
671
            $links['comment-add']['href'] = "node/$node->nid";
672
          }
Dries's avatar
 
Dries committed
673 674
        }
        else {
675
          $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node));
Dries's avatar
 
Dries committed
676 677 678
        }
      }
    }
Dries's avatar
Dries committed
679

680 681 682
    if (isset($links['comment_forbidden'])) {
      $links['comment_forbidden']['html'] = TRUE;
    }
683

684
    $node->content['links']['#links'] = array_merge($node->content['links']['#links'], $links);
Dries's avatar
Dries committed
685

686 687 688
    // Only append comments when we are building a node on its own node detail
    // page. We compare $node and $page_node to ensure that comments are not
    // appended to other nodes shown on the page, for example a node_reference
689
    // displayed in 'full' view mode within another node.
690
    if ($node->comment && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview) && user_access('access comments')) {
691
      $node->content['comments'] = comment_node_page_additions($node);
692
    }
693
  }
Dries's avatar
 
Dries committed
694 695
}

696 697 698 699 700 701
/**
 * Build the comment-related elements for node detail pages.
 *
 * @param $node
 *  A node object.
 */
702
function comment_node_page_additions($node) {
703 704 705 706 707 708
  $additions = array();

  // Only attempt to render comments if the node has visible comments.
  // Unpublished comments are not included in $node->comment_count, so show
  // comments unconditionally if the user is an administrator.
  if ($node->comment_count || user_access('administer comments')) {
709 710 711
    $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
    $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
    if ($cids = comment_get_thread($node, $mode, $comments_per_page)) {
712 713
      $comments = comment_load_multiple($cids);
      comment_prepare_thread($comments);
714
      $build = comment_view_multiple($comments, $node);
715 716 717 718 719 720 721
      $build['pager']['#theme'] = 'pager';
      $additions['comments'] = $build;
    }
  }

  // Append comment form if needed.
  if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) {
722
    $build = drupal_get_form('comment_form', (object) array('nid' => $node->nid));
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738
    $additions['comment_form'] = $build;
  }

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

  return $additions;
}

/**
739
 * Retrieve comments for a thread.
740 741 742
 *
 * @param $node
 *   The node whose comment(s) needs rendering.
743 744 745 746
 * @param $mode
 *   The comment display mode; COMMENT_MODE_FLAT or COMMENT_MODE_THREADED.
 * @param $comments_per_page
 *   The amount of comments to display per page.
747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801
 *
 * To display threaded comments in the correct order we keep a 'thread' field
 * and order by that value. This field keeps this data in
 * a way which is easy to update and convenient to use.
 *
 * A "thread" value starts at "1". If we add a child (A) to this comment,
 * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
 * brother of (A) will get "1.2". Next brother of the parent of (A) will get
 * "2" and so on.
 *
 * First of all note that the thread field stores the depth of the comment:
 * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
 *
 * Now to get the ordering right, consider this example:
 *
 * 1
 * 1.1
 * 1.1.1
 * 1.2
 * 2
 *
 * If we "ORDER BY thread ASC" we get the above result, and this is the
 * natural order sorted by time. However, if we "ORDER BY thread DESC"
 * we get:
 *
 * 2
 * 1.2
 * 1.1.1
 * 1.1
 * 1
 *
 * Clearly, this is not a natural way to see a thread, and users will get
 * confused. The natural order to show a thread by time desc would be:
 *
 * 2
 * 1
 * 1.2
 * 1.1
 * 1.1.1
 *
 * which is what we already did before the standard pager patch. To achieve
 * this we simply add a "/" at the end of each "thread" value. This way, the
 * thread fields will look like this:
 *
 * 1/
 * 1.1/
 * 1.1.1/
 * 1.2/
 * 2/
 *
 * we add "/" since this char is, in ASCII, higher than every number, so if
 * now we "ORDER BY thread DESC" we get the correct order. However this would
 * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
 * to consider the trailing "/" so we use a substring only.
 */
802
function comment_get_thread($node, $mode, $comments_per_page) {
803 804 805 806 807
  $query = db_select('comment', 'c')->extend('PagerDefault');
  $query->addField('c', 'cid');
  $query
    ->condition('c.nid', $node->nid)
    ->addTag('node_access')
808 809
    ->addTag('comment_filter')
    ->addMetaData('node', $node)
810 811 812 813 814 815
    ->limit($comments_per_page);

  $count_query = db_select('comment', 'c');
  $count_query->addExpression('COUNT(*)');
  $count_query
    ->condition('c.nid', $node->nid)
816 817 818
    ->addTag('node_access')
    ->addTag('comment_filter')
    ->addMetaData('node', $node);
819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887

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

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

  return $cids;
}

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

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

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

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

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

/**
 * Generate an array for rendering the given comment.
 *
 * @param $comment
 *   A comment object.
888 889
 * @param $node
 *   The node the comment is attached to.
890 891
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
892 893 894 895
 *
 * @return
 *   An array as expected by drupal_render().
 */
896
function comment_view($comment, $node, $view_mode = 'full') {
897
  // Populate $comment->content with a render() array.
898
  comment_build_content($comment, $node, $view_mode);
899 900

  $build = $comment->content;
901 902
  // We don't need duplicate rendering info in comment->content.
  unset($comment->content);
903 904 905 906

  $build += array(
    '#theme' => 'comment',
    '#comment' => $comment,
907
    '#node' => $node,
908
    '#view_mode' => $view_mode,
909 910
  );

911 912 913
  if (empty($comment->in_preview)) {
    $prefix = '';
    $is_threaded = isset($comment->divs) && variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED) == COMMENT_MODE_THREADED;
914

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

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

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

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

935
  // Allow modules to modify the structured comment.
936
  drupal_alter('comment_view', $build);
937

938 939 940 941 942 943 944
  return $build;
}

/**
 * Builds a structured array representing the comment's content.
 *
 * The content built for the comment (field values, comments, file attachments or
945
 * other comment components) will vary depending on the $view_mode parameter.
946 947 948
 *
 * @param $comment
 *   A comment object.
949 950
 * @param $node
 *   The node the comment is attached to.
951 952
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
953
 */
954
function comment_build_content($comment, $node, $view_mode = 'full') {
955 956
  // Remove previously built content, if exists.
  $comment->content = array();
957

958
  // Build fields content.
959
  field_attach_prepare_view('comment', array($comment->cid => $comment), $view_mode);
960
  entity_prepare_view('comment', array($comment->cid => $comment));
961
  $comment->content += field_attach_view('comment', $comment, $view_mode);
962

963 964
  if (empty($comment->in_preview)) {
    $comment->content['links']['comment'] = array(
965
      '#theme' => 'links__comment',
966
      '#links' => comment_links($comment, $node),
967
      '#attributes' => array('class' => array('links', 'inline')),
968 969 970 971
    );
  }

  // Allow modules to make their own additions to the comment.
972
  module_invoke_all('comment_view', $comment, $view_mode);
973 974
}

975 976 977 978 979 980 981
/**
 * Helper function, build links for an individual comment.
 *
 * Adds reply, edit, delete etc. depending on the current user permissions.
 *
 * @param $comment
 *   The comment object.
982 983
 * @param $node
 *   The node the comment is attached to.
984 985 986
 * @return
 *   A structured array of links.
 */
987
function comment_links($comment, $node) {
988 989 990
  $links = array();
  if ($node->comment == COMMENT_NODE_OPEN) {
    if (user_access('administer comments') && user_access('post comments')) {
991
      $links['comment-delete'] = array(
992
        'title' => t('delete'),
993
        'href' => "comment/$comment->cid/delete",