Projet

Général

Profil

Paste
Télécharger (91,1 ko) Statistiques
| Branche: | Révision:

root / drupal7 / modules / comment / comment.module @ 01dfd3b5

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
 * Implements callback_entity_info_uri().
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
  return MENU_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
  $view_mode = key(entity_view_mode_prepare('comment', array($comment->cid => $comment), $view_mode, $langcode));
997

    
998
  // Build fields content.
999
  field_attach_prepare_view('comment', array($comment->cid => $comment), $view_mode, $langcode);
1000
  entity_prepare_view('comment', array($comment->cid => $comment), $langcode);
1001
  $comment->content += field_attach_view('comment', $comment, $view_mode, $langcode);
1002

    
1003
  $comment->content['links'] = array(
1004
    '#theme' => 'links__comment',
1005
    '#pre_render' => array('drupal_pre_render_links'),
1006
    '#attributes' => array('class' => array('links', 'inline')),
1007
  );
1008
  if (empty($comment->in_preview)) {
1009
    $comment->content['links']['comment'] = array(
1010
      '#theme' => 'links__comment__comment',
1011
      '#links' => comment_links($comment, $node),
1012
      '#attributes' => array('class' => array('links', 'inline')),
1013
    );
1014
  }
1015

    
1016
  // Allow modules to make their own additions to the comment.
1017
  module_invoke_all('comment_view', $comment, $view_mode, $langcode);
1018
  module_invoke_all('entity_view', $comment, 'comment', $view_mode, $langcode);
1019

    
1020
  // Make sure the current view mode is stored if no module has already
1021
  // populated the related key.
1022
  $comment->content += array('#view_mode' => $view_mode);
1023
}
1024

    
1025
/**
1026
 * Helper function, build links for an individual comment.
1027
 *
1028
 * Adds reply, edit, delete etc. depending on the current user permissions.
1029
 *
1030
 * @param $comment
1031
 *   The comment object.
1032
 * @param $node
1033
 *   The node the comment is attached to.
1034
 * @return
1035
 *   A structured array of links.
1036
 */
1037
function comment_links($comment, $node) {
1038
  $links = array();
1039
  if ($node->comment == COMMENT_NODE_OPEN) {
1040
    if (user_access('administer comments') && user_access('post comments')) {
1041
      $links['comment-delete'] = array(
1042
        'title' => t('delete'),
1043
        'href' => "comment/$comment->cid/delete",
1044
        'html' => TRUE,
1045
      );
1046
      $links['comment-edit'] = array(
1047
        'title' => t('edit'),
1048
        'href' => "comment/$comment->cid/edit",
1049
        'html' => TRUE,
1050
      );
1051
      $links['comment-reply'] = array(
1052
        'title' => t('reply'),
1053
        'href' => "comment/reply/$comment->nid/$comment->cid",
1054
        'html' => TRUE,
1055
      );
1056
      if ($comment->status == COMMENT_NOT_PUBLISHED) {
1057
        $links['comment-approve'] = array(
1058
          'title' => t('approve'),
1059
          'href' => "comment/$comment->cid/approve",
1060
          'html' => TRUE,
1061
          'query' => array('token' => drupal_get_token("comment/$comment->cid/approve")),
1062
        );
1063
      }
1064
    }
1065
    elseif (user_access('post comments')) {
1066
      if (comment_access('edit', $comment)) {
1067
        $links['comment-edit'] = array(
1068
          'title' => t('edit'),
1069
          'href' => "comment/$comment->cid/edit",
1070
          'html' => TRUE,
1071
        );
1072
      }
1073
      $links['comment-reply'] = array(
1074
        'title' => t('reply'),
1075
        'href' => "comment/reply/$comment->nid/$comment->cid",
1076
        'html' => TRUE,
1077
      );
1078
    }
1079
    else {
1080
      $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node));
1081
      $links['comment_forbidden']['html'] = TRUE;
1082
    }
1083
  }
1084
  return $links;
1085
}
1086

    
1087
/**
1088
 * Construct a drupal_render() style array from an array of loaded comments.
1089
 *
1090
 * @param $comments
1091
 *   An array of comments as returned by comment_load_multiple().
1092
 * @param $node
1093
 *   The node the comments are attached to.
1094
 * @param $view_mode
1095
 *   View mode, e.g. 'full', 'teaser'...
1096
 * @param $weight
1097
 *   An integer representing the weight of the first comment in the list.
1098
 * @param $langcode
1099
 *   A string indicating the language field values are to be shown in. If no
1100
 *   language is provided the current content language is used.
1101
 *
1102
 * @return
1103
 *   An array in the format expected by drupal_render().
1104
 */
1105
function comment_view_multiple($comments, $node, $view_mode = 'full', $weight = 0, $langcode = NULL) {
1106
  $build = array();
1107
  $entities_by_view_mode = entity_view_mode_prepare('comment', $comments, $view_mode, $langcode);
1108
  foreach ($entities_by_view_mode as $entity_view_mode => $entities) {
1109
    field_attach_prepare_view('comment', $entities, $entity_view_mode, $langcode);
1110
    entity_prepare_view('comment', $entities, $langcode);
1111

    
1112
    foreach ($entities as $entity) {
1113
      $build[$entity->cid] = comment_view($entity, $node, $entity_view_mode, $langcode);
1114
    }
1115
  }
1116

    
1117
  foreach ($comments as $comment) {
1118
    $build[$comment->cid]['#weight'] = $weight;
1119
    $weight++;
1120
  }
1121
  // Sort here, to preserve the input order of the entities that were passed to
1122
  // this function.
1123
  uasort($build, 'element_sort');
1124
  $build['#sorted'] = TRUE;
1125

    
1126
  return $build;
1127
}
1128

    
1129
/**
1130
 * Implements hook_form_FORM_ID_alter().
1131
 */
1132
function comment_form_node_type_form_alter(&$form, $form_state) {
1133
  if (isset($form['type'])) {
1134
    $form['comment'] = array(
1135
      '#type' => 'fieldset',
1136
      '#title' => t('Comment settings'),
1137
      '#collapsible' => TRUE,
1138
      '#collapsed' => TRUE,
1139
      '#group' => 'additional_settings',
1140
      '#attributes' => array(
1141
        'class' => array('comment-node-type-settings-form'),
1142
      ),
1143
      '#attached' => array(
1144
        'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
1145
      ),
1146
    );
1147
    // Unlike coment_form_node_form_alter(), all of these settings are applied
1148
    // as defaults to all new nodes. Therefore, it would be wrong to use #states
1149
    // to hide the other settings based on the primary comment setting.
1150
    $form['comment']['comment'] = array(
1151
      '#type' => 'select',
1152
      '#title' => t('Default comment setting for new content'),
1153
      '#default_value' => variable_get('comment_' . $form['#node_type']->type, COMMENT_NODE_OPEN),
1154
      '#options' => array(
1155
        COMMENT_NODE_OPEN => t('Open'),
1156
        COMMENT_NODE_CLOSED => t('Closed'),
1157
        COMMENT_NODE_HIDDEN => t('Hidden'),
1158
      ),
1159
    );
1160
    $form['comment']['comment_default_mode'] = array(
1161
      '#type' => 'checkbox',
1162
      '#title' => t('Threading'),
1163
      '#default_value' => variable_get('comment_default_mode_' . $form['#node_type']->type, COMMENT_MODE_THREADED),
1164
      '#description' => t('Show comment replies in a threaded list.'),
1165
    );
1166
    $form['comment']['comment_default_per_page'] = array(
1167
      '#type' => 'select',
1168
      '#title' => t('Comments per page'),
1169
      '#default_value' => variable_get('comment_default_per_page_' . $form['#node_type']->type, 50),
1170
      '#options' => _comment_per_page(),
1171
    );
1172
    $form['comment']['comment_anonymous'] = array(
1173
      '#type' => 'select',
1174
      '#title' => t('Anonymous commenting'),
1175
      '#default_value' => variable_get('comment_anonymous_' . $form['#node_type']->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT),
1176
      '#options' => array(
1177
        COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'),
1178
        COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'),
1179
        COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information'),
1180
      ),
1181
      '#access' => user_access('post comments', drupal_anonymous_user()),
1182
    );
1183
    $form['comment']['comment_subject_field'] = array(
1184
      '#type' => 'checkbox',
1185
      '#title' => t('Allow comment title'),
1186
      '#default_value' => variable_get('comment_subject_field_' . $form['#node_type']->type, 1),
1187
    );
1188
    $form['comment']['comment_form_location'] = array(
1189
      '#type' => 'checkbox',
1190
      '#title' => t('Show reply form on the same page as comments'),
1191
      '#default_value' => variable_get('comment_form_location_' . $form['#node_type']->type, COMMENT_FORM_BELOW),
1192
    );
1193
    $form['comment']['comment_preview'] = array(
1194
      '#type' => 'radios',
1195
      '#title' => t('Preview comment'),
1196
      '#default_value' => variable_get('comment_preview_' . $form['#node_type']->type, DRUPAL_OPTIONAL),
1197
      '#options' => array(
1198
        DRUPAL_DISABLED => t('Disabled'),
1199
        DRUPAL_OPTIONAL => t('Optional'),
1200
        DRUPAL_REQUIRED => t('Required'),
1201
      ),
1202
    );
1203
  }
1204
}
1205

    
1206
/**
1207
 * Implements hook_form_BASE_FORM_ID_alter().
1208
 */
1209
function comment_form_node_form_alter(&$form, $form_state) {
1210
  $node = $form['#node'];
1211
  $form['comment_settings'] = array(
1212
    '#type' => 'fieldset',
1213
    '#access' => user_access('administer comments'),
1214
    '#title' => t('Comment settings'),
1215
    '#collapsible' => TRUE,
1216
    '#collapsed' => TRUE,
1217
    '#group' => 'additional_settings',
1218
    '#attributes' => array(
1219
      'class' => array('comment-node-settings-form'),
1220
    ),
1221
    '#attached' => array(
1222
      'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
1223
     ),
1224
    '#weight' => 30,
1225
  );
1226
  $comment_count = isset($node->nid) ? db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array(':nid' => $node->nid))->fetchField() : 0;
1227
  $comment_settings = ($node->comment == COMMENT_NODE_HIDDEN && empty($comment_count)) ? COMMENT_NODE_CLOSED : $node->comment;
1228
  $form['comment_settings']['comment'] = array(
1229
    '#type' => 'radios',
1230
    '#title' => t('Comments'),
1231
    '#title_display' => 'invisible',
1232
    '#parents' => array('comment'),
1233
    '#default_value' => $comment_settings,
1234
    '#options' => array(
1235
      COMMENT_NODE_OPEN => t('Open'),
1236
      COMMENT_NODE_CLOSED => t('Closed'),
1237
      COMMENT_NODE_HIDDEN => t('Hidden'),
1238
    ),
1239
    COMMENT_NODE_OPEN => array(
1240
      '#description' => t('Users with the "Post comments" permission can post comments.'),
1241
    ),
1242
    COMMENT_NODE_CLOSED => array(
1243
      '#description' => t('Users cannot post comments, but existing comments will be displayed.'),
1244
    ),
1245
    COMMENT_NODE_HIDDEN => array(
1246
      '#description' => t('Comments are hidden from view.'),
1247
    ),
1248
  );
1249
  // If the node doesn't have any comments, the "hidden" option makes no
1250
  // sense, so don't even bother presenting it to the user.
1251
  if (empty($comment_count)) {
1252
    $form['comment_settings']['comment'][COMMENT_NODE_HIDDEN]['#access'] = FALSE;
1253
    // Also adjust the description of the "closed" option.
1254
    $form['comment_settings']['comment'][COMMENT_NODE_CLOSED]['#description'] = t('Users cannot post comments.');
1255
  }
1256
}
1257

    
1258
/**
1259
 * Implements hook_node_load().
1260
 */
1261
function comment_node_load($nodes, $types) {
1262
  $comments_enabled = array();
1263

    
1264
  // Check if comments are enabled for each node. If comments are disabled,
1265
  // assign values without hitting the database.
1266
  foreach ($nodes as $node) {
1267
    // Store whether comments are enabled for this node.
1268
    if ($node->comment != COMMENT_NODE_HIDDEN) {
1269
      $comments_enabled[] = $node->nid;
1270
    }
1271
    else {
1272
      $node->cid = 0;
1273
      $node->last_comment_timestamp = $node->created;
1274
      $node->last_comment_name = '';
1275
      $node->last_comment_uid = $node->uid;
1276
      $node->comment_count = 0;
1277
    }
1278
  }
1279

    
1280
  // For nodes with comments enabled, fetch information from the database.
1281
  if (!empty($comments_enabled)) {
1282
    $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));
1283
    foreach ($result as $record) {
1284
      $nodes[$record->nid]->cid = $record->cid;
1285
      $nodes[$record->nid]->last_comment_timestamp = $record->last_comment_timestamp;
1286
      $nodes[$record->nid]->last_comment_name = $record->last_comment_name;
1287
      $nodes[$record->nid]->last_comment_uid = $record->last_comment_uid;
1288
      $nodes[$record->nid]->comment_count = $record->comment_count;
1289
    }
1290
  }
1291
}
1292

    
1293
/**
1294
 * Implements hook_node_prepare().
1295
 */
1296
function comment_node_prepare($node) {
1297
  if (!isset($node->comment)) {
1298
    $node->comment = variable_get("comment_$node->type", COMMENT_NODE_OPEN);
1299
  }
1300
}
1301

    
1302
/**
1303
 * Implements hook_node_insert().
1304
 */
1305
function comment_node_insert($node) {
1306
  // Allow bulk updates and inserts to temporarily disable the
1307
  // maintenance of the {node_comment_statistics} table.
1308
  if (variable_get('comment_maintain_node_statistics', TRUE)) {
1309
    db_insert('node_comment_statistics')
1310
      ->fields(array(
1311
        'nid' => $node->nid,
1312
        'cid' => 0,
1313
        'last_comment_timestamp' => $node->changed,
1314
        'last_comment_name' => NULL,
1315
        'last_comment_uid' => $node->uid,
1316
        'comment_count' => 0,
1317
      ))
1318
      ->execute();
1319
  }
1320
}
1321

    
1322
/**
1323
 * Implements hook_node_delete().
1324
 */
1325
function comment_node_delete($node) {
1326
  $cids = db_query('SELECT cid FROM {comment} WHERE nid = :nid', array(':nid' => $node->nid))->fetchCol();
1327
  comment_delete_multiple($cids);
1328
  db_delete('node_comment_statistics')
1329
    ->condition('nid', $node->nid)
1330
    ->execute();
1331
}
1332

    
1333
/**
1334
 * Implements hook_node_update_index().
1335
 */
1336
function comment_node_update_index($node) {
1337
  $index_comments = &drupal_static(__FUNCTION__);
1338

    
1339
  if ($index_comments === NULL) {
1340
    // Find and save roles that can 'access comments' or 'search content'.
1341
    $perms = array('access comments' => array(), 'search content' => array());
1342
    $result = db_query("SELECT rid, permission FROM {role_permission} WHERE permission IN ('access comments', 'search content')");
1343
    foreach ($result as $record) {
1344
      $perms[$record->permission][$record->rid] = $record->rid;
1345
    }
1346

    
1347
    // Prevent indexing of comments if there are any roles that can search but
1348
    // not view comments.
1349
    $index_comments = TRUE;
1350
    foreach ($perms['search content'] as $rid) {
1351
      if (!isset($perms['access comments'][$rid]) && ($rid <= DRUPAL_AUTHENTICATED_RID || !isset($perms['access comments'][DRUPAL_AUTHENTICATED_RID]))) {
1352
        $index_comments = FALSE;
1353
        break;
1354
      }
1355
    }
1356
  }
1357

    
1358
  if ($index_comments) {
1359
    $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
1360
    $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
1361
    if ($node->comment && $cids = comment_get_thread($node, $mode, $comments_per_page)) {
1362
      $comments = comment_load_multiple($cids);
1363
      comment_prepare_thread($comments);
1364
      $build = comment_view_multiple($comments, $node);
1365
      return drupal_render($build);
1366
    }
1367
  }
1368
  return '';
1369
}
1370

    
1371
/**
1372
 * Implements hook_update_index().
1373
 */
1374
function comment_update_index() {
1375
  // Store the maximum possible comments per thread (used for ranking by reply count)
1376
  variable_set('node_cron_comments_scale', 1.0 / max(1, db_query('SELECT MAX(comment_count) FROM {node_comment_statistics}')->fetchField()));
1377
}
1378

    
1379
/**
1380
 * Implements hook_node_search_result().
1381
 *
1382
 * Formats a comment count string and returns it, for display with search
1383
 * results.
1384
 */
1385
function comment_node_search_result($node) {
1386
  // Do not make a string if comments are hidden.
1387
  if (user_access('access comments') && $node->comment != COMMENT_NODE_HIDDEN) {
1388
    $comments = db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array('nid' => $node->nid))->fetchField();
1389
    // Do not make a string if comments are closed and there are currently
1390
    // zero comments.
1391
    if ($node->comment != COMMENT_NODE_CLOSED || $comments > 0) {
1392
      return array('comment' => format_plural($comments, '1 comment', '@count comments'));
1393
    }
1394
  }
1395
}
1396

    
1397
/**
1398
 * Implements hook_user_cancel().
1399
 */
1400
function comment_user_cancel($edit, $account, $method) {
1401
  switch ($method) {
1402
    case 'user_cancel_block_unpublish':
1403
      $comments = comment_load_multiple(array(), array('uid' => $account->uid));
1404
      foreach ($comments as $comment) {
1405
        $comment->status = 0;
1406
        comment_save($comment);
1407
      }
1408
      break;
1409

    
1410
    case 'user_cancel_reassign':
1411
      $comments = comment_load_multiple(array(), array('uid' => $account->uid));
1412
      foreach ($comments as $comment) {
1413
        $comment->uid = 0;
1414
        comment_save($comment);
1415
      }
1416
      break;
1417
  }
1418
}
1419

    
1420
/**
1421
 * Implements hook_user_delete().
1422
 */
1423
function comment_user_delete($account) {
1424
  $cids = db_query('SELECT c.cid FROM {comment} c WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol();
1425
  comment_delete_multiple($cids);
1426
}
1427

    
1428
/**
1429
 * Determines whether the current user has access to a particular comment.
1430
 *
1431
 * Authenticated users can edit their comments as long they have not been
1432
 * replied to. This prevents people from changing or revising their statements
1433
 * based on the replies to their posts.
1434
 *
1435
 * @param $op
1436
 *   The operation that is to be performed on the comment. Only 'edit' is
1437
 *   recognized now.
1438
 * @param $comment
1439
 *   The comment object.
1440
 * @return
1441
 *   TRUE if the current user has acces to the comment, FALSE otherwise.
1442
 */
1443
function comment_access($op, $comment) {
1444
  global $user;
1445

    
1446
  if ($op == 'edit') {
1447
    return ($user->uid && $user->uid == $comment->uid && $comment->status == COMMENT_PUBLISHED && user_access('edit own comments')) || user_access('administer comments');
1448
  }
1449
}
1450

    
1451
/**
1452
 * Accepts a submission of new or changed comment content.
1453
 *
1454
 * @param $comment
1455
 *   A comment object.
1456
 */
1457
function comment_save($comment) {
1458
  global $user;
1459

    
1460
  $transaction = db_transaction();
1461
  try {
1462
    $defaults = array(
1463
      'mail' => '',
1464
      'homepage' => '',
1465
      'name' => '',
1466
      'status' => user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED,
1467
    );
1468
    foreach ($defaults as $key => $default) {
1469
      if (!isset($comment->$key)) {
1470
        $comment->$key = $default;
1471
      }
1472
    }
1473
    // Make sure we have a bundle name.
1474
    if (!isset($comment->node_type)) {
1475
      $node = node_load($comment->nid);
1476
      $comment->node_type = 'comment_node_' . $node->type;
1477
    }
1478

    
1479
    // Load the stored entity, if any.
1480
    if (!empty($comment->cid) && !isset($comment->original)) {
1481
      $comment->original = entity_load_unchanged('comment', $comment->cid);
1482
    }
1483

    
1484
    field_attach_presave('comment', $comment);
1485

    
1486
    // Allow modules to alter the comment before saving.
1487
    module_invoke_all('comment_presave', $comment);
1488
    module_invoke_all('entity_presave', $comment, 'comment');
1489

    
1490
    if ($comment->cid) {
1491

    
1492
      drupal_write_record('comment', $comment, 'cid');
1493

    
1494
      // Ignore slave server temporarily to give time for the
1495
      // saved comment to be propagated to the slave.
1496
      db_ignore_slave();
1497

    
1498
      // Update the {node_comment_statistics} table prior to executing hooks.
1499
      _comment_update_node_statistics($comment->nid);
1500

    
1501
      field_attach_update('comment', $comment);
1502
      // Allow modules to respond to the updating of a comment.
1503
      module_invoke_all('comment_update', $comment);
1504
      module_invoke_all('entity_update', $comment, 'comment');
1505
    }
1506
    else {
1507
      // Add the comment to database. This next section builds the thread field.
1508
      // Also see the documentation for comment_view().
1509
      if (!empty($comment->thread)) {
1510
        // Allow calling code to set thread itself.
1511
        $thread = $comment->thread;
1512
      }
1513
      elseif ($comment->pid == 0) {
1514
        // This is a comment with no parent comment (depth 0): we start
1515
        // by retrieving the maximum thread level.
1516
        $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField();
1517
        // Strip the "/" from the end of the thread.
1518
        $max = rtrim($max, '/');
1519
        // We need to get the value at the correct depth.
1520
        $parts = explode('.', $max);
1521
        $firstsegment = $parts[0];
1522
        // Finally, build the thread field for this new comment.
1523
        $thread = int2vancode(vancode2int($firstsegment) + 1) . '/';
1524
      }
1525
      else {
1526
        // This is a comment with a parent comment, so increase the part of the
1527
        // thread value at the proper depth.
1528

    
1529
        // Get the parent comment:
1530
        $parent = comment_load($comment->pid);
1531
        // Strip the "/" from the end of the parent thread.
1532
        $parent->thread = (string) rtrim((string) $parent->thread, '/');
1533
        // Get the max value in *this* thread.
1534
        $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array(
1535
          ':thread' => $parent->thread . '.%',
1536
          ':nid' => $comment->nid,
1537
        ))->fetchField();
1538

    
1539
        if ($max == '') {
1540
          // First child of this parent.
1541
          $thread = $parent->thread . '.' . int2vancode(0) . '/';
1542
        }
1543
        else {
1544
          // Strip the "/" at the end of the thread.
1545
          $max = rtrim($max, '/');
1546
          // Get the value at the correct depth.
1547
          $parts = explode('.', $max);
1548
          $parent_depth = count(explode('.', $parent->thread));
1549
          $last = $parts[$parent_depth];
1550
          // Finally, build the thread field for this new comment.
1551
          $thread = $parent->thread . '.' . int2vancode(vancode2int($last) + 1) . '/';
1552
        }
1553
      }
1554

    
1555
      if (empty($comment->created)) {
1556
        $comment->created = REQUEST_TIME;
1557
      }
1558

    
1559
      if (empty($comment->changed)) {
1560
        $comment->changed = $comment->created;
1561
      }
1562

    
1563
      if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well.
1564
        $comment->name = $user->name;
1565
      }
1566

    
1567
      // Ensure the parent id (pid) has a value set.
1568
      if (empty($comment->pid)) {
1569
        $comment->pid = 0;
1570
      }
1571

    
1572
      // Add the values which aren't passed into the function.
1573
      $comment->thread = $thread;
1574
      $comment->hostname = ip_address();
1575

    
1576
      drupal_write_record('comment', $comment);
1577

    
1578
      // Ignore slave server temporarily to give time for the
1579
      // created comment to be propagated to the slave.
1580
      db_ignore_slave();
1581

    
1582
      // Update the {node_comment_statistics} table prior to executing hooks.
1583
      _comment_update_node_statistics($comment->nid);
1584

    
1585
      field_attach_insert('comment', $comment);
1586

    
1587
      // Tell the other modules a new comment has been submitted.
1588
      module_invoke_all('comment_insert', $comment);
1589
      module_invoke_all('entity_insert', $comment, 'comment');
1590
    }
1591
    if ($comment->status == COMMENT_PUBLISHED) {
1592
      module_invoke_all('comment_publish', $comment);
1593
    }
1594
    unset($comment->original);
1595
  }
1596
  catch (Exception $e) {
1597
    $transaction->rollback('comment');
1598
    watchdog_exception('comment', $e);
1599
    throw $e;
1600
  }
1601

    
1602
}
1603

    
1604
/**
1605
 * Delete a comment and all its replies.
1606
 *
1607
 * @param $cid
1608
 *   The comment to delete.
1609
 */
1610
function comment_delete($cid) {
1611
  comment_delete_multiple(array($cid));
1612
}
1613

    
1614
/**
1615
 * Delete comments and all their replies.
1616
 *
1617
 * @param $cids
1618
 *   The comment to delete.
1619
 */
1620
function comment_delete_multiple($cids) {
1621
  $comments = comment_load_multiple($cids);
1622
  if ($comments) {
1623
    $transaction = db_transaction();
1624
    try {
1625
      // Delete the comments.
1626
      db_delete('comment')
1627
        ->condition('cid', array_keys($comments), 'IN')
1628
        ->execute();
1629
      foreach ($comments as $comment) {
1630
        field_attach_delete('comment', $comment);
1631
        module_invoke_all('comment_delete', $comment);
1632
        module_invoke_all('entity_delete', $comment, 'comment');
1633

    
1634
        // Delete the comment's replies.
1635
        $child_cids = db_query('SELECT cid FROM {comment} WHERE pid = :cid', array(':cid' => $comment->cid))->fetchCol();
1636
        comment_delete_multiple($child_cids);
1637
        _comment_update_node_statistics($comment->nid);
1638
      }
1639
    }
1640
    catch (Exception $e) {
1641
      $transaction->rollback();
1642
      watchdog_exception('comment', $e);
1643
      throw $e;
1644
    }
1645
  }
1646
}
1647

    
1648
/**
1649
 * Load comments from the database.
1650
 *
1651
 * @param $cids
1652
 *   An array of comment IDs.
1653
 * @param $conditions
1654
 *   (deprecated) An associative array of conditions on the {comments}
1655
 *   table, where the keys are the database fields and the values are the
1656
 *   values those fields must have. Instead, it is preferable to use
1657
 *   EntityFieldQuery to retrieve a list of entity IDs loadable by
1658
 *   this function.
1659
 * @param $reset
1660
 *   Whether to reset the internal static entity cache. Note that the static
1661
 *   cache is disabled in comment_entity_info() by default.
1662
 *
1663
 * @return
1664
 *   An array of comment objects, indexed by comment ID.
1665
 *
1666
 * @see entity_load()
1667
 * @see EntityFieldQuery
1668
 *
1669
 * @todo Remove $conditions in Drupal 8.
1670
 */
1671
function comment_load_multiple($cids = array(), $conditions = array(), $reset = FALSE) {
1672
  return entity_load('comment', $cids, $conditions, $reset);
1673
}
1674

    
1675
/**
1676
 * Load the entire comment by cid.
1677
 *
1678
 * @param $cid
1679
 *   The identifying comment id.
1680
 * @param $reset
1681
 *   Whether to reset the internal static entity cache. Note that the static
1682
 *   cache is disabled in comment_entity_info() by default.
1683
 *
1684
 * @return
1685
 *   The comment object.
1686
 */
1687
function comment_load($cid, $reset = FALSE) {
1688
  $comment = comment_load_multiple(array($cid), array(), $reset);
1689
  return $comment ? $comment[$cid] : FALSE;
1690
}
1691

    
1692
/**
1693
 * Controller class for comments.
1694
 *
1695
 * This extends the DrupalDefaultEntityController class, adding required
1696
 * special handling for comment objects.
1697
 */
1698
class CommentController extends DrupalDefaultEntityController {
1699

    
1700
  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
1701
    $query = parent::buildQuery($ids, $conditions, $revision_id);
1702
    // Specify additional fields from the user and node tables.
1703
    $query->innerJoin('node', 'n', 'base.nid = n.nid');
1704
    $query->addField('n', 'type', 'node_type');
1705
    $query->innerJoin('users', 'u', 'base.uid = u.uid');
1706
    $query->addField('u', 'name', 'registered_name');
1707
    $query->fields('u', array('uid', 'signature', 'signature_format', 'picture'));
1708
    return $query;
1709
  }
1710

    
1711
  protected function attachLoad(&$comments, $revision_id = FALSE) {
1712
    // Setup standard comment properties.
1713
    foreach ($comments as $key => $comment) {
1714
      $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
1715
      $comment->new = node_mark($comment->nid, $comment->changed);
1716
      $comment->node_type = 'comment_node_' . $comment->node_type;
1717
      $comments[$key] = $comment;
1718
    }
1719
    parent::attachLoad($comments, $revision_id);
1720
  }
1721
}
1722

    
1723
/**
1724
 * Get number of new comments for current user and specified node.
1725
 *
1726
 * @param $nid
1727
 *   Node-id to count comments for.
1728
 * @param $timestamp
1729
 *   Time to count from (defaults to time of last user access
1730
 *   to node).
1731
 * @return The result or FALSE on error.
1732
 */
1733
function comment_num_new($nid, $timestamp = 0) {
1734
  global $user;
1735

    
1736
  if ($user->uid) {
1737
    // Retrieve the timestamp at which the current user last viewed this node.
1738
    if (!$timestamp) {
1739
      $timestamp = node_last_viewed($nid);
1740
    }
1741
    $timestamp = ($timestamp > NODE_NEW_LIMIT ? $timestamp : NODE_NEW_LIMIT);
1742

    
1743
    // Use the timestamp to retrieve the number of new comments.
1744
    return db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND created > :timestamp AND status = :status', array(
1745
      ':nid' => $nid,
1746
      ':timestamp' => $timestamp,
1747
      ':status' => COMMENT_PUBLISHED,
1748
      ))->fetchField();
1749
  }
1750
  else {
1751
    return FALSE;
1752
  }
1753

    
1754
}
1755

    
1756
/**
1757
 * Get the display ordinal for a comment, starting from 0.
1758
 *
1759
 * Count the number of comments which appear before the comment we want to
1760
 * display, taking into account display settings and threading.
1761
 *
1762
 * @param $cid
1763
 *   The comment ID.
1764
 * @param $node_type
1765
 *   The node type of the comment's parent.
1766
 * @return
1767
 *   The display ordinal for the comment.
1768
 * @see comment_get_display_page()
1769
 */
1770
function comment_get_display_ordinal($cid, $node_type) {
1771
  // Count how many comments (c1) are before $cid (c2) in display order. This is
1772
  // the 0-based display ordinal.
1773
  $query = db_select('comment', 'c1');
1774
  $query->innerJoin('comment', 'c2', 'c2.nid = c1.nid');
1775
  $query->addExpression('COUNT(*)', 'count');
1776
  $query->condition('c2.cid', $cid);
1777
  if (!user_access('administer comments')) {
1778
    $query->condition('c1.status', COMMENT_PUBLISHED);
1779
  }
1780
  $mode = variable_get('comment_default_mode_' . $node_type, COMMENT_MODE_THREADED);
1781

    
1782
  if ($mode == COMMENT_MODE_FLAT) {
1783
    // For flat comments, cid is used for ordering comments due to
1784
    // unpredicatable behavior with timestamp, so we make the same assumption
1785
    // here.
1786
    $query->condition('c1.cid', $cid, '<');
1787
  }
1788
  else {
1789
    // For threaded comments, the c.thread column is used for ordering. We can
1790
    // use the vancode for comparison, but must remove the trailing slash.
1791
    // See comment_view_multiple().
1792
    $query->where('SUBSTRING(c1.thread, 1, (LENGTH(c1.thread) -1)) < SUBSTRING(c2.thread, 1, (LENGTH(c2.thread) -1))');
1793
  }
1794

    
1795
  return $query->execute()->fetchField();
1796
}
1797

    
1798
/**
1799
 * Return the page number for a comment.
1800
 *
1801
 * Finds the correct page number for a comment taking into account display
1802
 * and paging settings.
1803
 *
1804
 * @param $cid
1805
 *   The comment ID.
1806
 * @param $node_type
1807
 *   The node type the comment is attached to.
1808
 * @return
1809
 *   The page number.
1810
 */
1811
function comment_get_display_page($cid, $node_type) {
1812
  $ordinal = comment_get_display_ordinal($cid, $node_type);
1813
  $comments_per_page = variable_get('comment_default_per_page_' . $node_type, 50);
1814
  return floor($ordinal / $comments_per_page);
1815
}
1816

    
1817
/**
1818
 * Page callback for comment editing.
1819
 */
1820
function comment_edit_page($comment) {
1821
  drupal_set_title(t('Edit comment %comment', array('%comment' => $comment->subject)), PASS_THROUGH);
1822
  $node = node_load($comment->nid);
1823
  return drupal_get_form("comment_node_{$node->type}_form", $comment);
1824
}
1825

    
1826
/**
1827
 * Implements hook_forms().
1828
 */
1829
function comment_forms() {
1830
  $forms = array();
1831
  foreach (node_type_get_types() as $type) {
1832
    $forms["comment_node_{$type->type}_form"]['callback'] = 'comment_form';
1833
  }
1834
  return $forms;
1835
}
1836

    
1837
/**
1838
 * Generate the basic commenting form, for appending to a node or display on a separate page.
1839
 *
1840
 * @see comment_form_validate()
1841
 * @see comment_form_submit()
1842
 *
1843
 * @ingroup forms
1844
 */
1845
function comment_form($form, &$form_state, $comment) {
1846
  global $user;
1847

    
1848
  // During initial form build, add the comment entity to the form state for
1849
  // use during form building and processing. During a rebuild, use what is in
1850
  // the form state.
1851
  if (!isset($form_state['comment'])) {
1852
    $defaults = array(
1853
      'name' => '',
1854
      'mail' => '',
1855
      'homepage' => '',
1856
      'subject' => '',
1857
      'comment' => '',
1858
      'cid' => NULL,
1859
      'pid' => NULL,
1860
      'language' => LANGUAGE_NONE,
1861
      'uid' => 0,
1862
    );
1863
    foreach ($defaults as $key => $value) {
1864
      if (!isset($comment->$key)) {
1865
        $comment->$key = $value;
1866
      }
1867
    }
1868
    $form_state['comment'] = $comment;
1869
  }
1870
  else {
1871
    $comment = $form_state['comment'];
1872
  }
1873

    
1874
  $node = node_load($comment->nid);
1875
  $form['#node'] = $node;
1876

    
1877
  // Use #comment-form as unique jump target, regardless of node type.
1878
  $form['#id'] = drupal_html_id('comment_form');
1879
  $form['#attributes']['class'][] = 'comment-form';
1880
  $form['#theme'] = array('comment_form__node_' . $node->type, 'comment_form');
1881

    
1882
  $anonymous_contact = variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT);
1883
  $is_admin = (!empty($comment->cid) && user_access('administer comments'));
1884

    
1885
  if (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
1886
    $form['#attached']['library'][] = array('system', 'jquery.cookie');
1887
    $form['#attributes']['class'][] = 'user-info-from-cookie';
1888
  }
1889

    
1890
  // If not replying to a comment, use our dedicated page callback for new
1891
  // comments on nodes.
1892
  if (empty($comment->cid) && empty($comment->pid)) {
1893
    $form['#action'] = url('comment/reply/' . $comment->nid);
1894
  }
1895

    
1896
  if (isset($form_state['comment_preview'])) {
1897
    $form += $form_state['comment_preview'];
1898
  }
1899

    
1900
  // Display author information in a fieldset for comment moderators.
1901
  if ($is_admin) {
1902
    $form['author'] = array(
1903
      '#type' => 'fieldset',
1904
      '#title' => t('Administration'),
1905
      '#collapsible' => TRUE,
1906
      '#collapsed' => TRUE,
1907
      '#weight' => -2,
1908
    );
1909
  }
1910
  else {
1911
    // Sets the author form elements above the subject.
1912
    $form['author'] = array(
1913
      '#weight' => -2,
1914
    );
1915
  }
1916

    
1917
  // Prepare default values for form elements.
1918
  if ($is_admin) {
1919
    $author = (!$comment->uid && $comment->name ? $comment->name : $comment->registered_name);
1920
    $status = (isset($comment->status) ? $comment->status : COMMENT_NOT_PUBLISHED);
1921
    $date = (!empty($comment->date) ? $comment->date : format_date($comment->created, 'custom', 'Y-m-d H:i O'));
1922
  }
1923
  else {
1924
    if ($user->uid) {
1925
      $author = $user->name;
1926
    }
1927
    else {
1928
      $author = ($comment->name ? $comment->name : '');
1929
    }
1930
    $status = (user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED);
1931
    $date = '';
1932
  }
1933

    
1934
  // Add the author name field depending on the current user.
1935
  if ($is_admin) {
1936
    $form['author']['name'] = array(
1937
      '#type' => 'textfield',
1938
      '#title' => t('Authored by'),
1939
      '#default_value' => $author,
1940
      '#maxlength' => 60,
1941
      '#size' => 30,
1942
      '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
1943
      '#autocomplete_path' => 'user/autocomplete',
1944
    );
1945
  }
1946
  elseif ($user->uid) {
1947
    $form['author']['_author'] = array(
1948
      '#type' => 'item',
1949
      '#title' => t('Your name'),
1950
      '#markup' => theme('username', array('account' => $user)),
1951
    );
1952
    $form['author']['name'] = array(
1953
      '#type' => 'value',
1954
      '#value' => $author,
1955
    );
1956
  }
1957
  else {
1958
    $form['author']['name'] = array(
1959
      '#type' => 'textfield',
1960
      '#title' => t('Your name'),
1961
      '#default_value' => $author,
1962
      '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
1963
      '#maxlength' => 60,
1964
      '#size' => 30,
1965
    );
1966
  }
1967

    
1968
  // Add author e-mail and homepage fields depending on the current user.
1969
  $form['author']['mail'] = array(
1970
    '#type' => 'textfield',
1971
    '#title' => t('E-mail'),
1972
    '#default_value' => $comment->mail,
1973
    '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
1974
    '#maxlength' => 64,
1975
    '#size' => 30,
1976
    '#description' => t('The content of this field is kept private and will not be shown publicly.'),
1977
    '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
1978
  );
1979
  $form['author']['homepage'] = array(
1980
    '#type' => 'textfield',
1981
    '#title' => t('Homepage'),
1982
    '#default_value' => $comment->homepage,
1983
    '#maxlength' => 255,
1984
    '#size' => 30,
1985
    '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
1986
  );
1987

    
1988
  // Add administrative comment publishing options.
1989
  $form['author']['date'] = array(
1990
    '#type' => 'textfield',
1991
    '#title' => t('Authored on'),
1992
    '#default_value' => $date,
1993
    '#maxlength' => 25,
1994
    '#size' => 20,
1995
    '#access' => $is_admin,
1996
  );
1997
  $form['author']['status'] = array(
1998
    '#type' => 'radios',
1999
    '#title' => t('Status'),
2000
    '#default_value' => $status,
2001
    '#options' => array(
2002
      COMMENT_PUBLISHED => t('Published'),
2003
      COMMENT_NOT_PUBLISHED => t('Not published'),
2004
    ),
2005
    '#access' => $is_admin,
2006
  );
2007

    
2008
  $form['subject'] = array(
2009
    '#type' => 'textfield',
2010
    '#title' => t('Subject'),
2011
    '#maxlength' => 64,
2012
    '#default_value' => $comment->subject,
2013
    '#access' => variable_get('comment_subject_field_' . $node->type, 1) == 1,
2014
    '#weight' => -1,
2015
  );
2016

    
2017
  // Used for conditional validation of author fields.
2018
  $form['is_anonymous'] = array(
2019
    '#type' => 'value',
2020
    '#value' => ($comment->cid ? !$comment->uid : !$user->uid),
2021
  );
2022

    
2023
  // Add internal comment properties.
2024
  foreach (array('cid', 'pid', 'nid', 'language', 'uid') as $key) {
2025
    $form[$key] = array('#type' => 'value', '#value' => $comment->$key);
2026
  }
2027
  $form['node_type'] = array('#type' => 'value', '#value' => 'comment_node_' . $node->type);
2028

    
2029
  // Only show the save button if comment previews are optional or if we are
2030
  // already previewing the submission.
2031
  $form['actions'] = array('#type' => 'actions');
2032
  $form['actions']['submit'] = array(
2033
    '#type' => 'submit',
2034
    '#value' => t('Save'),
2035
    '#access' => ($comment->cid && user_access('administer comments')) || variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || isset($form_state['comment_preview']),
2036
    '#weight' => 19,
2037
  );
2038
  $form['actions']['preview'] = array(
2039
    '#type' => 'submit',
2040
    '#value' => t('Preview'),
2041
    '#access' => (variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_DISABLED),
2042
    '#weight' => 20,
2043
    '#submit' => array('comment_form_build_preview'),
2044
  );
2045

    
2046
  // Attach fields.
2047
  $comment->node_type = 'comment_node_' . $node->type;
2048
  $langcode = entity_language('comment', $comment);
2049
  field_attach_form('comment', $comment, $form, $form_state, $langcode);
2050

    
2051
  return $form;
2052
}
2053

    
2054
/**
2055
 * Build a preview from submitted form values.
2056
 */
2057
function comment_form_build_preview($form, &$form_state) {
2058
  $comment = comment_form_submit_build_comment($form, $form_state);
2059
  $form_state['comment_preview'] = comment_preview($comment);
2060
  $form_state['rebuild'] = TRUE;
2061
}
2062

    
2063
/**
2064
 * Generate a comment preview.
2065
 */
2066
function comment_preview($comment) {
2067
  global $user;
2068

    
2069
  drupal_set_title(t('Preview comment'), PASS_THROUGH);
2070

    
2071
  $node = node_load($comment->nid);
2072

    
2073
  if (!form_get_errors()) {
2074
    $comment_body = field_get_items('comment', $comment, 'comment_body');
2075
    $comment->format = $comment_body[0]['format'];
2076
    // Attach the user and time information.
2077
    if (!empty($comment->name)) {
2078
      $account = user_load_by_name($comment->name);
2079
    }
2080
    elseif ($user->uid && empty($comment->is_anonymous)) {
2081
      $account = $user;
2082
    }
2083

    
2084
    if (!empty($account->uid)) {
2085
      $comment->uid = $account->uid;
2086
      $comment->name = check_plain($account->name);
2087
      $comment->signature = $account->signature;
2088
      $comment->signature_format = $account->signature_format;
2089
      $comment->picture = $account->picture;
2090
    }
2091
    elseif (empty($comment->name)) {
2092
      $comment->name = variable_get('anonymous', t('Anonymous'));
2093
    }
2094

    
2095
    $comment->created = !empty($comment->created) ? $comment->created : REQUEST_TIME;
2096
    $comment->changed = REQUEST_TIME;
2097
    $comment->in_preview = TRUE;
2098
    $comment_build = comment_view($comment, $node);
2099
    $comment_build['#weight'] = -100;
2100

    
2101
    $form['comment_preview'] = $comment_build;
2102
  }
2103

    
2104
  if ($comment->pid) {
2105
    $build = array();
2106
    if ($comments = comment_load_multiple(array($comment->pid), array('status' => COMMENT_PUBLISHED))) {
2107
      $parent_comment = $comments[$comment->pid];
2108
      $build = comment_view($parent_comment, $node);
2109
    }
2110
  }
2111
  else {
2112
    $build = node_view($node);
2113
  }
2114

    
2115
  $form['comment_output_below'] = $build;
2116
  $form['comment_output_below']['#weight'] = 100;
2117

    
2118
  return $form;
2119
}
2120

    
2121
/**
2122
 * Validate comment form submissions.
2123
 */
2124
function comment_form_validate($form, &$form_state) {
2125
  global $user;
2126

    
2127
  entity_form_field_validate('comment', $form, $form_state);
2128

    
2129
  if (!empty($form_state['values']['cid'])) {
2130
    // Verify the name in case it is being changed from being anonymous.
2131
    $account = user_load_by_name($form_state['values']['name']);
2132
    $form_state['values']['uid'] = $account ? $account->uid : 0;
2133

    
2134
    if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) {
2135
      form_set_error('date', t('You have to specify a valid date.'));
2136
    }
2137
    if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) {
2138
      form_set_error('name', t('You have to specify a valid author.'));
2139
    }
2140
  }
2141
  elseif ($form_state['values']['is_anonymous']) {
2142
    // Validate anonymous comment author fields (if given). If the (original)
2143
    // author of this comment was an anonymous user, verify that no registered
2144
    // user with this name exists.
2145
    if ($form_state['values']['name']) {
2146
      $query = db_select('users', 'u');
2147
      $query->addField('u', 'uid', 'uid');
2148
      $taken = $query
2149
        ->condition('name', db_like($form_state['values']['name']), 'LIKE')
2150
        ->countQuery()
2151
        ->execute()
2152
        ->fetchField();
2153
      if ($taken) {
2154
        form_set_error('name', t('The name you used belongs to a registered user.'));
2155
      }
2156
    }
2157
  }
2158
  if ($form_state['values']['mail'] && !valid_email_address($form_state['values']['mail'])) {
2159
    form_set_error('mail', t('The e-mail address you specified is not valid.'));
2160
  }
2161
  if ($form_state['values']['homepage'] && !valid_url($form_state['values']['homepage'], TRUE)) {
2162
    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>.'));
2163
  }
2164
}
2165

    
2166
/**
2167
 * Prepare a comment for submission.
2168
 */
2169
function comment_submit($comment) {
2170
  // @todo Legacy support. Remove in Drupal 8.
2171
  if (is_array($comment)) {
2172
    $comment += array('subject' => '');
2173
    $comment = (object) $comment;
2174
  }
2175

    
2176
  if (empty($comment->date)) {
2177
    $comment->date = 'now';
2178
  }
2179
  $comment->created = strtotime($comment->date);
2180
  $comment->changed = REQUEST_TIME;
2181

    
2182
  // If the comment was posted by a registered user, assign the author's ID.
2183
  // @todo Too fragile. Should be prepared and stored in comment_form() already.
2184
  if (!$comment->is_anonymous && !empty($comment->name) && ($account = user_load_by_name($comment->name))) {
2185
    $comment->uid = $account->uid;
2186
  }
2187
  // If the comment was posted by an anonymous user and no author name was
2188
  // required, use "Anonymous" by default.
2189
  if ($comment->is_anonymous && (!isset($comment->name) || $comment->name === '')) {
2190
    $comment->name = variable_get('anonymous', t('Anonymous'));
2191
  }
2192

    
2193
  // Validate the comment's subject. If not specified, extract from comment body.
2194
  if (trim($comment->subject) == '') {
2195
    // The body may be in any format, so:
2196
    // 1) Filter it into HTML
2197
    // 2) Strip out all HTML tags
2198
    // 3) Convert entities back to plain-text.
2199
    $field = field_info_field('comment_body');
2200
    $langcode = field_is_translatable('comment', $field) ? entity_language('comment', $comment) : LANGUAGE_NONE;
2201
    $comment_body = $comment->comment_body[$langcode][0];
2202
    if (isset($comment_body['format'])) {
2203
      $comment_text = check_markup($comment_body['value'], $comment_body['format']);
2204
    }
2205
    else {
2206
      $comment_text = check_plain($comment_body['value']);
2207
    }
2208
    $comment->subject = truncate_utf8(trim(decode_entities(strip_tags($comment_text))), 29, TRUE);
2209
    // Edge cases where the comment body is populated only by HTML tags will
2210
    // require a default subject.
2211
    if ($comment->subject == '') {
2212
      $comment->subject = t('(No subject)');
2213
    }
2214
  }
2215
  return $comment;
2216
}
2217

    
2218
/**
2219
 * Updates the form state's comment entity by processing this submission's values.
2220
 *
2221
 * This is the default builder function for the comment form. It is called
2222
 * during the "Save" and "Preview" submit handlers to retrieve the entity to
2223
 * save or preview. This function can also be called by a "Next" button of a
2224
 * wizard to update the form state's entity with the current step's values
2225
 * before proceeding to the next step.
2226
 *
2227
 * @see comment_form()
2228
 */
2229
function comment_form_submit_build_comment($form, &$form_state) {
2230
  $comment = $form_state['comment'];
2231
  entity_form_submit_build_entity('comment', $comment, $form, $form_state);
2232
  comment_submit($comment);
2233
  return $comment;
2234
}
2235

    
2236
/**
2237
 * Process comment form submissions; prepare the comment, store it, and set a redirection target.
2238
 */
2239
function comment_form_submit($form, &$form_state) {
2240
  $node = node_load($form_state['values']['nid']);
2241
  $comment = comment_form_submit_build_comment($form, $form_state);
2242
  if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) {
2243
    // Save the anonymous user information to a cookie for reuse.
2244
    if (user_is_anonymous()) {
2245
      user_cookie_save(array_intersect_key($form_state['values'], array_flip(array('name', 'mail', 'homepage'))));
2246
    }
2247

    
2248
    comment_save($comment);
2249
    $form_state['values']['cid'] = $comment->cid;
2250

    
2251
    // Add an entry to the watchdog log.
2252
    watchdog('content', 'Comment posted: %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)));
2253

    
2254
    // Explain the approval queue if necessary.
2255
    if ($comment->status == COMMENT_NOT_PUBLISHED) {
2256
      if (!user_access('administer comments')) {
2257
        drupal_set_message(t('Your comment has been queued for review by site administrators and will be published after approval.'));
2258
      }
2259
    }
2260
    else {
2261
      drupal_set_message(t('Your comment has been posted.'));
2262
    }
2263
    $query = array();
2264
    // Find the current display page for this comment.
2265
    $page = comment_get_display_page($comment->cid, $node->type);
2266
    if ($page > 0) {
2267
      $query['page'] = $page;
2268
    }
2269
    // Redirect to the newly posted comment.
2270
    $redirect = array('node/' . $node->nid, array('query' => $query, 'fragment' => 'comment-' . $comment->cid));
2271
  }
2272
  else {
2273
    watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject), WATCHDOG_WARNING);
2274
    drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject)), 'error');
2275
    // Redirect the user to the node they are commenting on.
2276
    $redirect = 'node/' . $node->nid;
2277
  }
2278
  $form_state['redirect'] = $redirect;
2279
  // Clear the block and page caches so that anonymous users see the comment
2280
  // they have posted.
2281
  cache_clear_all();
2282
}
2283

    
2284
/**
2285
 * Process variables for comment.tpl.php.
2286
 *
2287
 * @see comment.tpl.php
2288
 */
2289
function template_preprocess_comment(&$variables) {
2290
  $comment = $variables['elements']['#comment'];
2291
  $node = $variables['elements']['#node'];
2292
  $variables['comment']   = $comment;
2293
  $variables['node']      = $node;
2294
  $variables['author']    = theme('username', array('account' => $comment));
2295

    
2296
  $variables['created']   = format_date($comment->created);
2297

    
2298
  // Avoid calling format_date() twice on the same timestamp.
2299
  if ($comment->changed == $comment->created) {
2300
    $variables['changed'] = $variables['created'];
2301
  }
2302
  else {
2303
    $variables['changed'] = format_date($comment->changed);
2304
  }
2305

    
2306
  $variables['new']       = !empty($comment->new) ? t('new') : '';
2307
  $variables['picture']   = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', array('account' => $comment)) : '';
2308
  $variables['signature'] = $comment->signature;
2309

    
2310
  $uri = entity_uri('comment', $comment);
2311
  $uri['options'] += array('attributes' => array('class' => array('permalink'), 'rel' => 'bookmark'));
2312

    
2313
  $variables['title']     = l($comment->subject, $uri['path'], $uri['options']);
2314
  $variables['permalink'] = l(t('Permalink'), $uri['path'], $uri['options']);
2315
  $variables['submitted'] = t('Submitted by !username on !datetime', array('!username' => $variables['author'], '!datetime' => $variables['created']));
2316

    
2317
  // Preprocess fields.
2318
  field_attach_preprocess('comment', $comment, $variables['elements'], $variables);
2319

    
2320
  // Helpful $content variable for templates.
2321
  foreach (element_children($variables['elements']) as $key) {
2322
    $variables['content'][$key] = $variables['elements'][$key];
2323
  }
2324

    
2325
  // Set status to a string representation of comment->status.
2326
  if (isset($comment->in_preview)) {
2327
    $variables['status'] = 'comment-preview';
2328
  }
2329
  else {
2330
    $variables['status'] = ($comment->status == COMMENT_NOT_PUBLISHED) ? 'comment-unpublished' : 'comment-published';
2331
  }
2332

    
2333
  // Gather comment classes.
2334
  // 'comment-published' class is not needed, it is either 'comment-preview' or
2335
  // 'comment-unpublished'.
2336
  if ($variables['status'] != 'comment-published') {
2337
    $variables['classes_array'][] = $variables['status'];
2338
  }
2339
  if ($variables['new']) {
2340
    $variables['classes_array'][] = 'comment-new';
2341
  }
2342
  if (!$comment->uid) {
2343
    $variables['classes_array'][] = 'comment-by-anonymous';
2344
  }
2345
  else {
2346
    if ($comment->uid == $variables['node']->uid) {
2347
      $variables['classes_array'][] = 'comment-by-node-author';
2348
    }
2349
    if ($comment->uid == $variables['user']->uid) {
2350
      $variables['classes_array'][] = 'comment-by-viewer';
2351
    }
2352
  }
2353
}
2354

    
2355
/**
2356
 * Returns HTML for a "you can't post comments" notice.
2357
 *
2358
 * @param $variables
2359
 *   An associative array containing:
2360
 *   - node: The comment node.
2361
 *
2362
 * @ingroup themeable
2363
 */
2364
function theme_comment_post_forbidden($variables) {
2365
  $node = $variables['node'];
2366
  global $user;
2367

    
2368
  // Since this is expensive to compute, we cache it so that a page with many
2369
  // comments only has to query the database once for all the links.
2370
  $authenticated_post_comments = &drupal_static(__FUNCTION__, NULL);
2371

    
2372
  if (!$user->uid) {
2373
    if (!isset($authenticated_post_comments)) {
2374
      // We only output a link if we are certain that users will get permission
2375
      // to post comments by logging in.
2376
      $comment_roles = user_roles(TRUE, 'post comments');
2377
      $authenticated_post_comments = isset($comment_roles[DRUPAL_AUTHENTICATED_RID]);
2378
    }
2379

    
2380
    if ($authenticated_post_comments) {
2381
      // We cannot use drupal_get_destination() because these links
2382
      // sometimes appear on /node and taxonomy listing pages.
2383
      if (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_SEPARATE_PAGE) {
2384
        $destination = array('destination' => "comment/reply/$node->nid#comment-form");
2385
      }
2386
      else {
2387
        $destination = array('destination' => "node/$node->nid#comment-form");
2388
      }
2389

    
2390
      if (variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)) {
2391
        // Users can register themselves.
2392
        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))));
2393
      }
2394
      else {
2395
        // Only admins can add new users, no public registration.
2396
        return t('<a href="@login">Log in</a> to post comments', array('@login' => url('user/login', array('query' => $destination))));
2397
      }
2398
    }
2399
  }
2400
}
2401

    
2402
/**
2403
 * Process variables for comment-wrapper.tpl.php.
2404
 *
2405
 * @see comment-wrapper.tpl.php
2406
 */
2407
function template_preprocess_comment_wrapper(&$variables) {
2408
  // Provide contextual information.
2409
  $variables['node'] = $variables['content']['#node'];
2410
  $variables['display_mode'] = variable_get('comment_default_mode_' . $variables['node']->type, COMMENT_MODE_THREADED);
2411
  // The comment form is optional and may not exist.
2412
  $variables['content'] += array('comment_form' => array());
2413
}
2414

    
2415
/**
2416
 * Return an array of viewing modes for comment listings.
2417
 *
2418
 * We can't use a global variable array because the locale system
2419
 * is not initialized yet when the comment module is loaded.
2420
 */
2421
function _comment_get_modes() {
2422
  return array(
2423
    COMMENT_MODE_FLAT => t('Flat list'),
2424
    COMMENT_MODE_THREADED => t('Threaded list')
2425
  );
2426
}
2427

    
2428
/**
2429
 * Return an array of "comments per page" settings from which the user
2430
 * can choose.
2431
 */
2432
function _comment_per_page() {
2433
  return drupal_map_assoc(array(10, 30, 50, 70, 90, 150, 200, 250, 300));
2434
}
2435

    
2436
/**
2437
 * Updates the comment statistics for a given node. This should be called any
2438
 * time a comment is added, deleted, or updated.
2439
 *
2440
 * The following fields are contained in the node_comment_statistics table.
2441
 * - last_comment_timestamp: the timestamp of the last comment for this node or the node create stamp if no comments exist for the node.
2442
 * - last_comment_name: the name of the anonymous poster for the last comment
2443
 * - 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.
2444
 * - comment_count: the total number of approved/published comments on this node.
2445
 */
2446
function _comment_update_node_statistics($nid) {
2447
  // Allow bulk updates and inserts to temporarily disable the
2448
  // maintenance of the {node_comment_statistics} table.
2449
  if (!variable_get('comment_maintain_node_statistics', TRUE)) {
2450
    return;
2451
  }
2452

    
2453
  $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array(
2454
    ':nid' => $nid,
2455
    ':status' => COMMENT_PUBLISHED,
2456
  ))->fetchField();
2457

    
2458
  if ($count > 0) {
2459
    // Comments exist.
2460
    $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(
2461
      ':nid' => $nid,
2462
      ':status' => COMMENT_PUBLISHED,
2463
    ))->fetchObject();
2464
    db_update('node_comment_statistics')
2465
      ->fields(array(
2466
        'cid' => $last_reply->cid,
2467
        'comment_count' => $count,
2468
        'last_comment_timestamp' => $last_reply->changed,
2469
        'last_comment_name' => $last_reply->uid ? '' : $last_reply->name,
2470
        'last_comment_uid' => $last_reply->uid,
2471
      ))
2472
      ->condition('nid', $nid)
2473
      ->execute();
2474
  }
2475
  else {
2476
    // Comments do not exist.
2477
    $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject();
2478
    db_update('node_comment_statistics')
2479
      ->fields(array(
2480
        'cid' => 0,
2481
        'comment_count' => 0,
2482
        'last_comment_timestamp' => $node->created,
2483
        'last_comment_name' => '',
2484
        'last_comment_uid' => $node->uid,
2485
      ))
2486
      ->condition('nid', $nid)
2487
      ->execute();
2488
  }
2489
}
2490

    
2491
/**
2492
 * Generate vancode.
2493
 *
2494
 * Consists of a leading character indicating length, followed by N digits
2495
 * with a numerical value in base 36. Vancodes can be sorted as strings
2496
 * without messing up numerical order.
2497
 *
2498
 * It goes:
2499
 * 00, 01, 02, ..., 0y, 0z,
2500
 * 110, 111, ... , 1zy, 1zz,
2501
 * 2100, 2101, ..., 2zzy, 2zzz,
2502
 * 31000, 31001, ...
2503
 */
2504
function int2vancode($i = 0) {
2505
  $num = base_convert((int) $i, 10, 36);
2506
  $length = strlen($num);
2507

    
2508
  return chr($length + ord('0') - 1) . $num;
2509
}
2510

    
2511
/**
2512
 * Decode vancode back to an integer.
2513
 */
2514
function vancode2int($c = '00') {
2515
  return base_convert(substr($c, 1), 36, 10);
2516
}
2517

    
2518
/**
2519
 * Implements hook_action_info().
2520
 */
2521
function comment_action_info() {
2522
  return array(
2523
    'comment_publish_action' => array(
2524
      'label' => t('Publish comment'),
2525
      'type' => 'comment',
2526
      'configurable' => FALSE,
2527
      'behavior' => array('changes_property'),
2528
      'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
2529
    ),
2530
    'comment_unpublish_action' => array(
2531
      'label' => t('Unpublish comment'),
2532
      'type' => 'comment',
2533
      'configurable' => FALSE,
2534
      'behavior' => array('changes_property'),
2535
      'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
2536
    ),
2537
    'comment_unpublish_by_keyword_action' => array(
2538
      'label' => t('Unpublish comment containing keyword(s)'),
2539
      'type' => 'comment',
2540
      'configurable' => TRUE,
2541
      'behavior' => array('changes_property'),
2542
      'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
2543
    ),
2544
    'comment_save_action' => array(
2545
      'label' => t('Save comment'),
2546
      'type' => 'comment',
2547
      'configurable' => FALSE,
2548
      'triggers' => array('comment_insert', 'comment_update'),
2549
    ),
2550
  );
2551
}
2552

    
2553
/**
2554
 * Publishes a comment.
2555
 *
2556
 * @param $comment
2557
 *   An optional comment object.
2558
 * @param array $context
2559
 *   Array with components:
2560
 *   - 'cid': Comment ID. Required if $comment is not given.
2561
 *
2562
 * @ingroup actions
2563
 */
2564
function comment_publish_action($comment, $context = array()) {
2565
  if (isset($comment->subject)) {
2566
    $subject = $comment->subject;
2567
    $comment->status = COMMENT_PUBLISHED;
2568
  }
2569
  else {
2570
    $cid = $context['cid'];
2571
    $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField();
2572
    db_update('comment')
2573
      ->fields(array('status' => COMMENT_PUBLISHED))
2574
      ->condition('cid', $cid)
2575
      ->execute();
2576
  }
2577
  watchdog('action', 'Published comment %subject.', array('%subject' => $subject));
2578
}
2579

    
2580
/**
2581
 * Unpublishes a comment.
2582
 *
2583
 * @param $comment
2584
 *   An optional comment object.
2585
 * @param array $context
2586
 *   Array with components:
2587
 *   - 'cid': Comment ID. Required if $comment is not given.
2588
 *
2589
 * @ingroup actions
2590
 */
2591
function comment_unpublish_action($comment, $context = array()) {
2592
  if (isset($comment->subject)) {
2593
    $subject = $comment->subject;
2594
    $comment->status = COMMENT_NOT_PUBLISHED;
2595
  }
2596
  else {
2597
    $cid = $context['cid'];
2598
    $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField();
2599
    db_update('comment')
2600
      ->fields(array('status' => COMMENT_NOT_PUBLISHED))
2601
      ->condition('cid', $cid)
2602
      ->execute();
2603
  }
2604
  watchdog('action', 'Unpublished comment %subject.', array('%subject' => $subject));
2605
}
2606

    
2607
/**
2608
 * Unpublishes a comment if it contains certain keywords.
2609
 *
2610
 * @param object $comment
2611
 *   Comment object to modify.
2612
 * @param array $context
2613
 *   Array with components:
2614
 *   - 'keywords': Keywords to look for. If the comment contains at least one
2615
 *     of the keywords, it is unpublished.
2616
 *
2617
 * @ingroup actions
2618
 * @see comment_unpublish_by_keyword_action_form()
2619
 * @see comment_unpublish_by_keyword_action_submit()
2620
 */
2621
function comment_unpublish_by_keyword_action($comment, $context) {
2622
  $node = node_load($comment->nid);
2623
  $build = comment_view($comment, $node);
2624
  $text = drupal_render($build);
2625
  foreach ($context['keywords'] as $keyword) {
2626
    if (strpos($text, $keyword) !== FALSE) {
2627
      $comment->status = COMMENT_NOT_PUBLISHED;
2628
      comment_save($comment);
2629
      watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject));
2630
      break;
2631
    }
2632
  }
2633
}
2634

    
2635
/**
2636
 * Form builder; Prepare a form for blacklisted keywords.
2637
 *
2638
 * @ingroup forms
2639
 * @see comment_unpublish_by_keyword_action()
2640
 * @see comment_unpublish_by_keyword_action_submit()
2641
 */
2642
function comment_unpublish_by_keyword_action_form($context) {
2643
  $form['keywords'] = array(
2644
    '#title' => t('Keywords'),
2645
    '#type' => 'textarea',
2646
    '#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."'),
2647
    '#default_value' => isset($context['keywords']) ? drupal_implode_tags($context['keywords']) : '',
2648
  );
2649

    
2650
  return $form;
2651
}
2652

    
2653
/**
2654
 * Process comment_unpublish_by_keyword_action_form form submissions.
2655
 *
2656
 * @see comment_unpublish_by_keyword_action()
2657
 */
2658
function comment_unpublish_by_keyword_action_submit($form, $form_state) {
2659
  return array('keywords' => drupal_explode_tags($form_state['values']['keywords']));
2660
}
2661

    
2662
/**
2663
 * Saves a comment.
2664
 *
2665
 * @ingroup actions
2666
 */
2667
function comment_save_action($comment) {
2668
  comment_save($comment);
2669
  cache_clear_all();
2670
  watchdog('action', 'Saved comment %title', array('%title' => $comment->subject));
2671
}
2672

    
2673
/**
2674
 * Implements hook_ranking().
2675
 */
2676
function comment_ranking() {
2677
  return array(
2678
    'comments' => array(
2679
      'title' => t('Number of comments'),
2680
      'join' => array(
2681
        'type' => 'LEFT',
2682
        'table' => 'node_comment_statistics',
2683
        'alias' => 'node_comment_statistics',
2684
        'on' => 'node_comment_statistics.nid = i.sid',
2685
      ),
2686
      // Inverse law that maps the highest reply count on the site to 1 and 0 to 0.
2687
      'score' => '2.0 - 2.0 / (1.0 + node_comment_statistics.comment_count * CAST(:scale AS DECIMAL))',
2688
      'arguments' => array(':scale' => variable_get('node_cron_comments_scale', 0)),
2689
    ),
2690
  );
2691
}
2692

    
2693
/**
2694
 * Implements hook_rdf_mapping().
2695
 */
2696
function comment_rdf_mapping() {
2697
  return array(
2698
    array(
2699
      'type' => 'comment',
2700
      'bundle' => RDF_DEFAULT_BUNDLE,
2701
      'mapping' => array(
2702
        'rdftype' => array('sioc:Post', 'sioct:Comment'),
2703
        'title' => array(
2704
          'predicates' => array('dc:title'),
2705
        ),
2706
        'created' => array(
2707
          'predicates' => array('dc:date', 'dc:created'),
2708
          'datatype' => 'xsd:dateTime',
2709
          'callback' => 'date_iso8601',
2710
        ),
2711
        'changed' => array(
2712
          'predicates' => array('dc:modified'),
2713
          'datatype' => 'xsd:dateTime',
2714
          'callback' => 'date_iso8601',
2715
        ),
2716
        'comment_body' => array(
2717
          'predicates' => array('content:encoded'),
2718
        ),
2719
        'pid' => array(
2720
          'predicates' => array('sioc:reply_of'),
2721
          'type' => 'rel',
2722
        ),
2723
        'uid' => array(
2724
          'predicates' => array('sioc:has_creator'),
2725
          'type' => 'rel',
2726
        ),
2727
        'name' => array(
2728
          'predicates' => array('foaf:name'),
2729
        ),
2730
      ),
2731
    ),
2732
  );
2733
}
2734

    
2735
/**
2736
 * Implements hook_file_download_access().
2737
 */
2738
function comment_file_download_access($field, $entity_type, $entity) {
2739
  if ($entity_type == 'comment') {
2740
    if (user_access('access comments') && $entity->status == COMMENT_PUBLISHED || user_access('administer comments')) {
2741
      $node = node_load($entity->nid);
2742
      return node_access('view', $node);
2743
    }
2744
    return FALSE;
2745
  }
2746
}