Projet

Général

Profil

Paste
Télécharger (90,7 ko) Statistiques
| Branche: | Révision:

root / htmltest / modules / comment / comment.module @ 85ad3d82

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
  $context = array(
997
    'entity_type' => 'comment',
998
    'entity' => $comment,
999
    'langcode' => $langcode,
1000
  );
1001
  drupal_alter('entity_view_mode', $view_mode, $context);
1002

    
1003
  // Build fields content.
1004
  field_attach_prepare_view('comment', array($comment->cid => $comment), $view_mode, $langcode);
1005
  entity_prepare_view('comment', array($comment->cid => $comment), $langcode);
1006
  $comment->content += field_attach_view('comment', $comment, $view_mode, $langcode);
1007

    
1008
  $comment->content['links'] = array(
1009
    '#theme' => 'links__comment',
1010
    '#pre_render' => array('drupal_pre_render_links'),
1011
    '#attributes' => array('class' => array('links', 'inline')),
1012
  );
1013
  if (empty($comment->in_preview)) {
1014
    $comment->content['links']['comment'] = array(
1015
      '#theme' => 'links__comment__comment',
1016
      '#links' => comment_links($comment, $node),
1017
      '#attributes' => array('class' => array('links', 'inline')),
1018
    );
1019
  }
1020

    
1021
  // Allow modules to make their own additions to the comment.
1022
  module_invoke_all('comment_view', $comment, $view_mode, $langcode);
1023
  module_invoke_all('entity_view', $comment, 'comment', $view_mode, $langcode);
1024

    
1025
  // Make sure the current view mode is stored if no module has already
1026
  // populated the related key.
1027
  $comment->content += array('#view_mode' => $view_mode);
1028
}
1029

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

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

    
1114
  $build = array(
1115
    '#sorted' => TRUE,
1116
  );
1117
  foreach ($comments as $comment) {
1118
    $build[$comment->cid] = comment_view($comment, $node, $view_mode, $langcode);
1119
    $build[$comment->cid]['#weight'] = $weight;
1120
    $weight++;
1121
  }
1122
  return $build;
1123
}
1124

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

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

    
1254
/**
1255
 * Implements hook_node_load().
1256
 */
1257
function comment_node_load($nodes, $types) {
1258
  $comments_enabled = array();
1259

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

    
1276
  // For nodes with comments enabled, fetch information from the database.
1277
  if (!empty($comments_enabled)) {
1278
    $result = db_query('SELECT nid, cid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count FROM {node_comment_statistics} WHERE nid IN (:comments_enabled)', array(':comments_enabled' => $comments_enabled));
1279
    foreach ($result as $record) {
1280
      $nodes[$record->nid]->cid = $record->cid;
1281
      $nodes[$record->nid]->last_comment_timestamp = $record->last_comment_timestamp;
1282
      $nodes[$record->nid]->last_comment_name = $record->last_comment_name;
1283
      $nodes[$record->nid]->last_comment_uid = $record->last_comment_uid;
1284
      $nodes[$record->nid]->comment_count = $record->comment_count;
1285
    }
1286
  }
1287
}
1288

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

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

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

    
1329
/**
1330
 * Implements hook_node_update_index().
1331
 */
1332
function comment_node_update_index($node) {
1333
  $index_comments = &drupal_static(__FUNCTION__);
1334

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

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

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

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

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

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

    
1406
    case 'user_cancel_reassign':
1407
      $comments = comment_load_multiple(array(), array('uid' => $account->uid));
1408
      foreach ($comments as $comment) {
1409
        $comment->uid = 0;
1410
        comment_save($comment);
1411
      }
1412
      break;
1413
  }
1414
}
1415

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

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

    
1442
  if ($op == 'edit') {
1443
    return ($user->uid && $user->uid == $comment->uid && $comment->status == COMMENT_PUBLISHED && user_access('edit own comments')) || user_access('administer comments');
1444
  }
1445
}
1446

    
1447
/**
1448
 * Accepts a submission of new or changed comment content.
1449
 *
1450
 * @param $comment
1451
 *   A comment object.
1452
 */
1453
function comment_save($comment) {
1454
  global $user;
1455

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

    
1475
    // Load the stored entity, if any.
1476
    if (!empty($comment->cid) && !isset($comment->original)) {
1477
      $comment->original = entity_load_unchanged('comment', $comment->cid);
1478
    }
1479

    
1480
    field_attach_presave('comment', $comment);
1481

    
1482
    // Allow modules to alter the comment before saving.
1483
    module_invoke_all('comment_presave', $comment);
1484
    module_invoke_all('entity_presave', $comment, 'comment');
1485

    
1486
    if ($comment->cid) {
1487

    
1488
      drupal_write_record('comment', $comment, 'cid');
1489

    
1490
      // Ignore slave server temporarily to give time for the
1491
      // saved comment to be propagated to the slave.
1492
      db_ignore_slave();
1493

    
1494
      // Update the {node_comment_statistics} table prior to executing hooks.
1495
      _comment_update_node_statistics($comment->nid);
1496

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

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

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

    
1551
      if (empty($comment->created)) {
1552
        $comment->created = REQUEST_TIME;
1553
      }
1554

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

    
1559
      if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well.
1560
        $comment->name = $user->name;
1561
      }
1562

    
1563
      // Ensure the parent id (pid) has a value set.
1564
      if (empty($comment->pid)) {
1565
        $comment->pid = 0;
1566
      }
1567

    
1568
      // Add the values which aren't passed into the function.
1569
      $comment->thread = $thread;
1570
      $comment->hostname = ip_address();
1571

    
1572
      drupal_write_record('comment', $comment);
1573

    
1574
      // Ignore slave server temporarily to give time for the
1575
      // created comment to be propagated to the slave.
1576
      db_ignore_slave();
1577

    
1578
      // Update the {node_comment_statistics} table prior to executing hooks.
1579
      _comment_update_node_statistics($comment->nid);
1580

    
1581
      field_attach_insert('comment', $comment);
1582

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

    
1598
}
1599

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

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

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

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

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

    
1688
/**
1689
 * Controller class for comments.
1690
 *
1691
 * This extends the DrupalDefaultEntityController class, adding required
1692
 * special handling for comment objects.
1693
 */
1694
class CommentController extends DrupalDefaultEntityController {
1695

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

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

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

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

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

    
1750
}
1751

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

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

    
1791
  return $query->execute()->fetchField();
1792
}
1793

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

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

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

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

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

    
1870
  $node = node_load($comment->nid);
1871
  $form['#node'] = $node;
1872

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

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

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

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

    
1892
  if (isset($form_state['comment_preview'])) {
1893
    $form += $form_state['comment_preview'];
1894
  }
1895

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

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

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

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

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

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

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

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

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

    
2042
  // Attach fields.
2043
  $comment->node_type = 'comment_node_' . $node->type;
2044
  $langcode = entity_language('comment', $comment);
2045
  field_attach_form('comment', $comment, $form, $form_state, $langcode);
2046

    
2047
  return $form;
2048
}
2049

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

    
2059
/**
2060
 * Generate a comment preview.
2061
 */
2062
function comment_preview($comment) {
2063
  global $user;
2064

    
2065
  drupal_set_title(t('Preview comment'), PASS_THROUGH);
2066

    
2067
  $node = node_load($comment->nid);
2068

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

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

    
2091
    $comment->created = !empty($comment->created) ? $comment->created : REQUEST_TIME;
2092
    $comment->changed = REQUEST_TIME;
2093
    $comment->in_preview = TRUE;
2094
    $comment_build = comment_view($comment, $node);
2095
    $comment_build['#weight'] = -100;
2096

    
2097
    $form['comment_preview'] = $comment_build;
2098
  }
2099

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

    
2111
  $form['comment_output_below'] = $build;
2112
  $form['comment_output_below']['#weight'] = 100;
2113

    
2114
  return $form;
2115
}
2116

    
2117
/**
2118
 * Validate comment form submissions.
2119
 */
2120
function comment_form_validate($form, &$form_state) {
2121
  global $user;
2122

    
2123
  entity_form_field_validate('comment', $form, $form_state);
2124

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

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

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

    
2172
  if (empty($comment->date)) {
2173
    $comment->date = 'now';
2174
  }
2175
  $comment->created = strtotime($comment->date);
2176
  $comment->changed = REQUEST_TIME;
2177

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

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

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

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

    
2244
    comment_save($comment);
2245
    $form_state['values']['cid'] = $comment->cid;
2246

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

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

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

    
2292
  $variables['created']   = format_date($comment->created);
2293

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

    
2302
  $variables['new']       = !empty($comment->new) ? t('new') : '';
2303
  $variables['picture']   = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', array('account' => $comment)) : '';
2304
  $variables['signature'] = $comment->signature;
2305

    
2306
  $uri = entity_uri('comment', $comment);
2307
  $uri['options'] += array('attributes' => array('class' => 'permalink', 'rel' => 'bookmark'));
2308

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

    
2313
  // Preprocess fields.
2314
  field_attach_preprocess('comment', $comment, $variables['elements'], $variables);
2315

    
2316
  // Helpful $content variable for templates.
2317
  foreach (element_children($variables['elements']) as $key) {
2318
    $variables['content'][$key] = $variables['elements'][$key];
2319
  }
2320

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

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

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

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

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

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

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

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

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

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

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

    
2449
  $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array(
2450
    ':nid' => $nid,
2451
    ':status' => COMMENT_PUBLISHED,
2452
  ))->fetchField();
2453

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

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

    
2504
  return chr($length + ord('0') - 1) . $num;
2505
}
2506

    
2507
/**
2508
 * Decode vancode back to an integer.
2509
 */
2510
function vancode2int($c = '00') {
2511
  return base_convert(substr($c, 1), 36, 10);
2512
}
2513

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

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

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

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

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

    
2643
  return $form;
2644
}
2645

    
2646
/**
2647
 * Process comment_unpublish_by_keyword_action_form form submissions.
2648
 *
2649
 * @see comment_unpublish_by_keyword_action()
2650
 */
2651
function comment_unpublish_by_keyword_action_submit($form, $form_state) {
2652
  return array('keywords' => drupal_explode_tags($form_state['values']['keywords']));
2653
}
2654

    
2655
/**
2656
 * Saves a comment.
2657
 *
2658
 * @ingroup actions
2659
 */
2660
function comment_save_action($comment) {
2661
  comment_save($comment);
2662
  cache_clear_all();
2663
  watchdog('action', 'Saved comment %title', array('%title' => $comment->subject));
2664
}
2665

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

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

    
2728
/**
2729
 * Implements hook_file_download_access().
2730
 */
2731
function comment_file_download_access($field, $entity_type, $entity) {
2732
  if ($entity_type == 'comment') {
2733
    if (user_access('access comments') && $entity->status == COMMENT_PUBLISHED || user_access('administer comments')) {
2734
      $node = node_load($entity->nid);
2735
      return node_access('view', $node);
2736
    }
2737
    return FALSE;
2738
  }
2739
}