comment.module 60.7 KB
Newer Older
1
<?php
Dries's avatar
 
Dries committed
2

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

13
use Drupal\Core\Entity\EntityInterface;
14
15
16
17
18
19
20
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\comment\CommentInterface;
use Drupal\comment\Entity\Comment;
use Drupal\entity\Entity\EntityDisplay;
use Drupal\field\FieldInstanceInterface;
use Drupal\field\FieldInterface;
use Drupal\file\FileInterface;
21

22
/**
23
 * Comment is awaiting approval.
24
 */
25
const COMMENT_NOT_PUBLISHED = 0;
26
27

/**
28
 * Comment is published.
29
 */
30
const COMMENT_PUBLISHED = 1;
Dries's avatar
Dries committed
31

32
33
34
/**
 * Comments are displayed in a flat list - expanded.
 */
35
const COMMENT_MODE_FLAT = 0;
36
37
38
39

/**
 * Comments are displayed as a threaded list - expanded.
 */
40
const COMMENT_MODE_THREADED = 1;
Dries's avatar
Dries committed
41
42

/**
43
 * Anonymous posters cannot enter their contact information.
Dries's avatar
Dries committed
44
 */
45
const COMMENT_ANONYMOUS_MAYNOT_CONTACT = 0;
46
47
48
49

/**
 * Anonymous posters may leave their contact information.
 */
50
const COMMENT_ANONYMOUS_MAY_CONTACT = 1;
51
52

/**
53
 * Anonymous posters are required to leave their contact information.
54
 */
55
const COMMENT_ANONYMOUS_MUST_CONTACT = 2;
Dries's avatar
Dries committed
56
57

/**
58
 * Comment form should be displayed on a separate page.
Dries's avatar
Dries committed
59
 */
60
const COMMENT_FORM_SEPARATE_PAGE = 0;
61
62
63
64

/**
 * Comment form should be shown below post or list of comments.
 */
65
const COMMENT_FORM_BELOW = 1;
Dries's avatar
Dries committed
66
67

/**
68
 * Comments for this entity are hidden.
Dries's avatar
Dries committed
69
 */
70
const COMMENT_HIDDEN = 0;
71
72

/**
73
 * Comments for this entity are closed.
74
 */
75
const COMMENT_CLOSED = 1;
76
77

/**
78
 * Comments for this entity are open.
79
 */
80
const COMMENT_OPEN = 2;
81

82
83
84
85
86
87
88
89
90
91
/**
 * The time cutoff for comments marked as read for entity types other node.
 *
 * Comments changed before this time are always marked as read.
 * Comments changed after this time may be marked new, updated, or read,
 * depending on their state for the current user. Defaults to 30 days ago.
 *
 * @todo Remove when http://drupal.org/node/1029708 lands.
 */
define('COMMENT_NEW_LIMIT', REQUEST_TIME - 30 * 24 * 60 * 60);
92

93
/**
94
 * Implements hook_help().
95
 */
96
97
function comment_help($path, $arg) {
  switch ($path) {
Dries's avatar
   
Dries committed
98
    case 'admin/help#comment':
99
      $output = '<h3>' . t('About') . '</h3>';
100
      $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>';
101
102
103
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Default and custom settings') . '</dt>';
104
      $output .= '<dd>' . t("Comment functionality can be attached to any Drupal entity, eg. a content <a href='@content-type'>type</a> and the behavior can be customised to suit. Each entity 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 entity, 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>';
105
106
      $output .= '<dt>' . t('Comment approval') . '</dt>';
      $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>';
107
      $output .= '</dl>';
108
      return $output;
109
110
111
112

    case 'admin/structure/comments':
      $output = '<p>' . t('This page provides a list of all comment forms on the site and allows you to manage the fields, form and display settings for each.') . '</p>';
      return $output;
113
  }
Dries's avatar
   
Dries committed
114
115
}

116
117
118
119
120
/**
 * Implements hook_entity_bundle_info().
 */
function comment_entity_bundle_info() {
  $bundles = array();
121
122
123
124
125
126
127
128
129
  foreach (\Drupal::service('comment.manager')->getAllFields() as $entity_type => $fields) {
    foreach ($fields as $field_name => $field_info) {
      $sample_bundle = reset($field_info['bundles']);
      // We cannot use field info API here because it will result in recursion.
      $config = \Drupal::config('field.instance.' . $entity_type .  '.' . $sample_bundle . '.' . $field_name);
      $bundles['comment'][$entity_type . '__' . $field_name] = array(
        'label' => $config->get('label'),
      );
    }
130
  }
131
  return $bundles;
132
133
}

134
/**
135
 * Entity URI callback.
136
 */
137
function comment_uri(Comment $comment) {
138
  return array(
139
140
    'path' => 'comment/' . $comment->id(),
    'options' => array('fragment' => 'comment-' . $comment->id()),
141
  );
142
143
}

144
145
146
147
148
/**
 * Implements hook_field_extra_fields().
 */
function comment_field_extra_fields() {
  $return = array();
149
150
151
  foreach (\Drupal::service('comment.manager')->getAllFields() as $entity_type => $fields) {
    foreach ($fields as $field_name => $field_info) {
      $return['comment'][$entity_type . '__' . $field_name] = array(
152
153
154
155
156
157
        'form' => array(
          'author' => array(
            'label' => t('Author'),
            'description' => t('Author textfield'),
            'weight' => -2,
          ),
158
          'subject' => array(
159
160
161
162
            'label' => t('Subject'),
            'description' => t('Subject textfield'),
            'weight' => -1,
          ),
163
164
165
166
167
168
169
170
        ),
      );
    }
  }

  return $return;
}

171
/**
172
 * Implements hook_theme().
173
174
175
176
 */
function comment_theme() {
  return array(
    'comment_block' => array(
177
      'variables' => array('number' => NULL),
178
179
    ),
    'comment_preview' => array(
180
      'variables' => array('comment' => NULL),
181
182
    ),
    'comment' => array(
183
      'template' => 'comment',
184
      'render element' => 'elements',
185
186
    ),
    'comment_post_forbidden' => array(
187
      'variables' => array('commented_entity' => NULL, 'field_name' => 'comment'),
188
189
    ),
    'comment_wrapper' => array(
190
      'template' => 'comment-wrapper',
191
      'render element' => 'content',
192
193
194
195
    ),
  );
}

Dries's avatar
   
Dries committed
196
/**
197
 * Implements hook_menu().
Dries's avatar
   
Dries committed
198
 */
199
function comment_menu() {
200
201
202
203
204
205
206
207
208
  $items['admin/structure/comments'] = array(
    'title' => 'Comment forms',
    'description' => 'Manage fields and displays settings for comment forms.',
    'route_name' => 'comment.bundle_list',
  );
  $items['admin/structure/comments/manage/%'] = array(
    'title' => 'Comment form',
    'route_name' => 'comment.bundle',
  );
209
  $items['admin/content/comment'] = array(
210
    'title' => 'Comments',
211
    'description' => 'List and edit site comments and the comment approval queue.',
212
    'route_name' => 'comment.admin',
213
    'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM,
214
  );
215
  // Tabs begin here.
216
  $items['admin/content/comment/new'] = array(
217
    'title' => 'Published comments',
218
219
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
220
  $items['admin/content/comment/approval'] = array(
221
222
    'title' => 'Unapproved comments',
    'title callback' => 'comment_count_unpublished',
223
    'route_name' => 'comment.admin_approval',
224
225
    'type' => MENU_LOCAL_TASK,
  );
Dries's avatar
   
Dries committed
226
227
228
229

  return $items;
}

230
231
232
233
/**
 * Implements hook_menu_alter().
 */
function comment_menu_alter(&$items) {
234
235
236
237
  if (isset($items['admin/content'])) {
    // Add comments to the description for admin/content if any.
    $items['admin/content']['description'] = 'Administer content and comments.';
  }
238
239
}

240
241
242
243
244
245
246
247
248
249
/**
 * Returns a menu title which includes the number of unapproved comments.
 */
function comment_count_unpublished() {
  $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE status = :status', array(
    ':status' => COMMENT_NOT_PUBLISHED,
  ))->fetchField();
  return t('Unapproved comments (@count)', array('@count' => $count));
}

250
/**
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
 * Implements hook_ENTITY_TYPE_create() for 'field_instance'.
 */
function comment_field_instance_create(FieldInstanceInterface $instance) {
  if ($instance->getFieldType() == 'comment') {
    \Drupal::service('comment.manager')->addBodyField($instance->entity_type, $instance->getFieldName());
    \Drupal::cache()->delete('comment_entity_info');
    // Assign default values for the field instance.
    $instance->default_value = array(array(
      'status' => COMMENT_OPEN,
      'cid' => 0,
      'last_comment_timestamp' => 0,
      'last_comment_name' => '',
      'last_comment_uid' => 0,
      'comment_count' => 0,
    ));
  }
267
}
268

269
/**
270
 * Implements hook_ENTITY_TYPE_update() for 'field_instance'.
271
 */
272
273
function comment_field_instance_update(FieldInstanceInterface $instance) {
  if ($instance->getFieldType() == 'comment') {
274
    \Drupal::entityManager()->getViewBuilder($instance->entity_type)->resetCache();
275
276
  }
}
277

278
/**
279
 * Implements hook_ENTITY_TYPE_delete() for 'field_entity'.
280
 */
281
282
283
284
285
function comment_field_entity_delete(FieldInterface $field) {
  if ($field->getFieldType() == 'comment') {
    // Delete all fields and displays attached to the comment bundle.
    entity_invoke_bundle_hook('delete', 'comment', $field->getFieldName());
    \Drupal::cache()->delete('comment_entity_info');
286
287
288
  }
}

289
290
291
292
293
294
295
296
297
298
299
300
/**
 * Implements hook_ENTITY_TYPE_delete() for 'field_instance'.
 */
function comment_field_instance_delete(FieldInstanceInterface $instance) {
  if ($instance->getFieldType() == 'comment') {
    // Delete all comments that used by the entity bundle.
    $comments = db_query("SELECT cid FROM {comment} WHERE entity_type = :entity_type AND field_id = :field_id", array(
      ':entity_type' => $instance->entityType(),
      ':field_id' => $instance->entityType() . '__' . $instance->getFieldName(),
    ))->fetchCol();
    entity_delete_multiple('comment', $comments);
    \Drupal::cache()->delete('comment_entity_info');
301
  }
302
303
}

Dries's avatar
   
Dries committed
304
/**
305
 * Implements hook_permission().
Dries's avatar
   
Dries committed
306
 */
307
function comment_permission() {
308
  return array(
309
    'administer comments' => array(
310
      'title' => t('Administer comments and comment settings'),
311
312
    ),
    'access comments' => array(
313
      'title' => t('View comments'),
314
315
    ),
    'post comments' => array(
316
      'title' => t('Post comments'),
317
    ),
318
319
    'skip comment approval' => array(
      'title' => t('Skip comment approval'),
320
    ),
321
322
323
    'edit own comments' => array(
      'title' => t('Edit own comments'),
    ),
324
  );
Dries's avatar
   
Dries committed
325
326
}

327
/**
328
 * Finds the most recent comments that are available to the current user.
329
330
 *
 * @param integer $number
331
 *   (optional) The maximum number of comments to find. Defaults to 10.
332
 *
333
 * @return
334
335
 *   An array of comment objects or an empty array if there are no recent
 *   comments visible to the current user.
336
337
 */
function comment_get_recent($number = 10) {
338
  $query = db_select('comment', 'c');
339
  $query->addMetaData('base_table', 'comment');
340
341
342
343
344
345
  $query->fields('c')
    ->condition('c.status', COMMENT_PUBLISHED);
  if (\Drupal::moduleHandler()->moduleExists('node')) {
    // Special case to filter by published content.
    $query->innerJoin('node_field_data', 'n', "n.nid = c.entity_id AND c.entity_type = 'node'");
    $query->addTag('node_access');
346
347
    // @todo This should be actually filtering on the desired node status field
    //   language and just fall back to the default language.
348
349
350
351
352
    $query
      ->condition('n.status', NODE_PUBLISHED)
      ->condition('n.default_langcode', 1);
  }
  $comments = $query
353
    ->orderBy('c.created', 'DESC')
354
355
356
    // Additionally order by cid to ensure that comments with the same timestamp
    // are returned in the exact order posted.
    ->orderBy('c.cid', 'DESC')
357
358
359
360
361
    ->range(0, $number)
    ->execute()
    ->fetchAll();

  return $comments ? $comments : array();
362
363
}

364
/**
365
 * Calculates the page number for the first new comment.
366
 *
367
 * @param int $num_comments
368
 *   Number of comments.
369
 * @param int $new_replies
370
 *   Number of new replies.
371
372
373
374
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The first new comment entity.
 * @param string $field_name
 *   The field name on the entity to which comments are attached to.
375
 *
376
377
 * @return
 *   "page=X" if the page number is greater than zero; empty string otherwise.
378
 */
379
380
381
382
function comment_new_page_count($num_comments, $new_replies, EntityInterface $entity, $field_name = 'comment') {
  $instance = \Drupal::service('field.info')->getInstance($entity->entityType(), $entity->bundle(), $field_name);
  $mode = $instance->getFieldSetting('default_mode');
  $comments_per_page = $instance->getFieldSetting('per_page');
383
  $pagenum = NULL;
384
  $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
385
386
  if ($num_comments <= $comments_per_page) {
    // Only one page of comments.
387
    $pageno = 0;
388
  }
389
390
391
  elseif ($flat) {
    // Flat comments.
    $count = $num_comments - $new_replies;
392
    $pageno = $count / $comments_per_page;
393
  }
394
  else {
395
396
397
398
399
400
    // Threaded comments: we build a query with a subquery to find the first
    // thread with a new comment.

    // 1. Find all the threads with a new comment.
    $unread_threads_query = db_select('comment')
      ->fields('comment', array('thread'))
401
402
403
      ->condition('entity_id', $entity->id())
      ->condition('entity_type', $entity->entityType())
      ->condition('field_id', $entity->entityType() . '__' . $field_name)
404
      ->condition('status', COMMENT_PUBLISHED)
405
406
      ->orderBy('created', 'DESC')
      ->orderBy('cid', 'DESC')
407
408
409
410
411
412
413
414
415
416
417
418
419
420
      ->range(0, $new_replies);

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

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

    // Find the number of the first comment of the first unread thread.
421
422
423
424
    $count = db_query('SELECT COUNT(*) FROM {comment} WHERE entity_id = :entity_id
                      AND entity_type = :entity_type
                      AND field_id = :field_id
                      AND status = :status AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread', array(
425
      ':status' => COMMENT_PUBLISHED,
426
427
428
      ':entity_id' => $entity->id(),
      ':field_id' => $entity->entityType() . '__' . $field_name,
      ':entity_type' => $entity->entityType(),
429
      ':thread' => $first_thread,
430
    ))->fetchField();
431

432
    $pageno = $count / $comments_per_page;
433
  }
434

435
  if ($pageno >= 1) {
436
    $pagenum = array('page' => intval($pageno));
437
  }
438

439
440
441
  return $pagenum;
}

442
/**
443
 * Returns HTML for a list of recent comments.
444
445
446
 *
 * @ingroup themeable
 */
447
function theme_comment_block($variables) {
448
  $items = array();
449
  $number = $variables['number'];
450
  foreach (comment_get_recent($number) as $comment) {
451
    $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>';
452
  }
453

454
  if ($items) {
455
456
457
458
459
    $item_list = array(
      '#theme' => 'item_list',
      '#items' => $items,
    );
    return drupal_render($item_list);
460
  }
461
462
463
  else {
    return t('No comments available.');
  }
464
465
}

Dries's avatar
   
Dries committed
466
/**
467
 * Implements hook_entity_view().
Dries's avatar
   
Dries committed
468
 */
469
470
471
472
473
474
475
476
477
478
479
480
function comment_entity_view(EntityInterface $entity, EntityDisplay $display, $view_mode, $langcode) {
  if ($entity->entityType() != 'node') {
    // Comment links are only added to node entity type for backwards
    // compatibility. Should you require comment links for other entity types
    // you can do-so by implementing a new field formatter.
    // @todo Make this configurable from the formatter see
    //   http://drupal.org/node/1901110
    return;
  }
  $fields = \Drupal::service('comment.manager')->getFields('node');
  foreach ($fields as $field_name => $detail) {
    // Skip fields that entity does not have.
481
    if (!$entity->hasField($field_name)) {
482
      continue;
483
    }
484
485
486
487
488
489
490
491
492
493
494
    $links = array();
    $commenting_status = $entity->get($field_name)->status;
    if ($commenting_status) {
      $instance = \Drupal::service('field.info')->getInstance('node', $entity->bundle(), $field_name);
      // Entity have commenting open or close.
      $uri = $entity->uri();
      if ($view_mode == 'rss') {
        // Add a comments RSS element which is a URL to the comments of this node.
        if (!empty($uri['options'])) {
          $uri['options']['fragment'] = 'comments';
          $uri['options']['absolute'] = TRUE;
Dries's avatar
   
Dries committed
495
        }
496
497
498
499
        $entity->rss_elements[] = array(
          'key' => 'comments',
          'value' => url($uri['path'], $uri['options'])
        );
500
      }
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
      elseif ($view_mode == 'teaser') {
        // Teaser view: display the number of comments that have been posted,
        // or a link to add new comments if the user has permission, the node
        // is open to new comments, and there currently are none.
        if (user_access('access comments')) {
          if (!empty($entity->get($field_name)->comment_count)) {
            $links['comment-comments'] = array(
              'title' => format_plural($entity->get($field_name)->comment_count, '1 comment', '@count comments'),
              'href' => $uri['path'],
              'attributes' => array('title' => t('Jump to the first comment of this posting.')),
              'fragment' => 'comments',
              'html' => TRUE,
            );
            if (\Drupal::moduleHandler()->moduleExists('history')) {
              $links['comment-new-comments'] = array(
                'title' => '',
                'href' => '',
                'attributes' => array(
                  'class' => 'hidden',
                  'title' => t('Jump to the first new comment of this posting.'),
                  'data-history-node-last-comment-timestamp' => $entity->get($field_name)->last_comment_timestamp,
                  'data-history-node-field-name' => $field_name,
                ),
                'html' => TRUE,
              );
            }
527
          }
528
        }
529
530
531
532
        // Provide a link to new comment form.
        if ($commenting_status == COMMENT_OPEN) {
          $comment_form_location = $instance->getFieldSetting('form_location');
          if (user_access('post comments')) {
533
534
            $links['comment-add'] = array(
              'title' => t('Add new comment'),
535
536
              'href' => $uri['path'],
              'attributes' => array('title' => t('Add a new comment to this page.')),
537
538
539
              'fragment' => 'comment-form',
            );
            if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
540
              $links['comment-add']['href'] = 'comment/reply/'. $entity->entityType() . '/' . $entity->id() .'/' . $field_name;
541
            }
542
          }
543
544
545
546
547
548
549
550
551
552
553
          else {
            $comment_post_forbidden = array(
              '#theme' => 'comment_post_forbidden',
              '#commented_entity' => $entity,
              '#field_name' => $field_name,
            );
            $links['comment-forbidden'] = array(
              'title' => drupal_render($comment_post_forbidden),
              'html' => TRUE,
            );
          }
Dries's avatar
   
Dries committed
554
        }
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
      }
      elseif ($view_mode != 'search_index' && $view_mode != 'search_result') {
        // Entity in other view modes: add a "post comment" link if the user is
        // allowed to post comments and if this entity is allowing new comments.
        // But we don't want this link if we're building the entity for search
        // indexing or constructing a search result excerpt.
        if ($commenting_status == COMMENT_OPEN) {
          $comment_form_location = $instance->getFieldSetting('form_location');
          if (user_access('post comments')) {
            // Show the "post comment" link if the form is on another page, or
            // if there are existing comments that the link will skip past.
            if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE || (!empty($entity->get($field_name)->comment_count) && user_access('access comments'))) {
              $links['comment-add'] = array(
                'title' => t('Add new comment'),
                'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
                'href' => $uri['path'],
                'fragment' => 'comment-form',
              );
              if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
                $links['comment-add']['href'] = 'comment/reply/'. $entity->entityType() . '/' . $entity->id() .'/' . $field_name;
              }
            }
          }
          else {
            $comment_post_forbidden = array(
              '#theme' => 'comment_post_forbidden',
              '#commented_entity' => $entity,
              '#field_name' => $field_name,
            );
            $links['comment-forbidden'] = array(
              'title' => drupal_render($comment_post_forbidden),
              'html' => TRUE,
            );
          }
Dries's avatar
   
Dries committed
589
590
591
        }
      }
    }
Dries's avatar
Dries committed
592

593
594
    $entity->content['links']['comment__' . $field_name] = array(
      '#theme' => 'links__entity__comment__' . $field_name,
595
596
597
      '#links' => $links,
      '#attributes' => array('class' => array('links', 'inline')),
    );
598
    if ($view_mode == 'teaser' && \Drupal::moduleHandler()->moduleExists('history') && \Drupal::currentUser()->isAuthenticated()) {
599
      $entity->content['links']['#attached']['library'][] = array('comment', 'drupal.node-new-comments-link');
600
    }
601
  }
Dries's avatar
   
Dries committed
602
603
}

604
605
606
607
/**
 * Implements hook_node_view_alter().
 */
function comment_node_view_alter(&$build, EntityInterface $node, EntityDisplay $display) {
608
  if (\Drupal::moduleHandler()->moduleExists('history')) {
609
610
611
612
    $build['#attributes']['data-history-node-id'] = $node->id();
  }
}

613
/**
614
 * Returns a rendered form to comment the given entity.
615
 *
616
617
618
619
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity to which the comments are in reply to.
 * @param string $field_name
 *   The field name where the comments were entered.
620
621
622
 * @param int $pid
 *   (optional) Some comments are replies to other comments. In those cases,
 *   $pid is the parent comment's comment ID. Defaults to NULL.
623
624
625
626
 *
 * @return array
 *   The renderable array for the comment addition form.
 */
627
628
629
630
631
632
633
function comment_add(EntityInterface $entity, $field_name = 'comment', $pid = NULL) {
  $values = array(
    'entity_type' => $entity->entityType(),
    'entity_id' => $entity->id(),
    'field_id' => $entity->entityType() . '__' . $field_name,
    'pid' => $pid,
  );
634
  $comment = entity_create('comment', $values);
635
  return \Drupal::entityManager()->getForm($comment);
636
637
}

638
/**
639
 * Retrieves comments for a thread.
640
 *
641
642
643
644
645
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity whose comment(s) needs rendering.
 * @param string $field_name
 *   The field_name whose comment(s) needs rendering.
 * @param int $mode
646
 *   The comment display mode; COMMENT_MODE_FLAT or COMMENT_MODE_THREADED.
647
 * @param int $comments_per_page
648
 *   The amount of comments to display per page.
649
 *
650
651
652
 * @return
 *   An array of the IDs of the comment to be displayed.
 *
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
 * To display threaded comments in the correct order we keep a 'thread' field
 * and order by that value. This field keeps this data in
 * a way which is easy to update and convenient to use.
 *
 * A "thread" value starts at "1". If we add a child (A) to this comment,
 * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
 * brother of (A) will get "1.2". Next brother of the parent of (A) will get
 * "2" and so on.
 *
 * First of all note that the thread field stores the depth of the comment:
 * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
 *
 * Now to get the ordering right, consider this example:
 *
 * 1
 * 1.1
 * 1.1.1
 * 1.2
 * 2
 *
 * If we "ORDER BY thread ASC" we get the above result, and this is the
 * natural order sorted by time. However, if we "ORDER BY thread DESC"
 * we get:
 *
 * 2
 * 1.2
 * 1.1.1
 * 1.1
 * 1
 *
 * Clearly, this is not a natural way to see a thread, and users will get
 * confused. The natural order to show a thread by time desc would be:
 *
 * 2
 * 1
 * 1.2
 * 1.1
 * 1.1.1
 *
 * which is what we already did before the standard pager patch. To achieve
 * this we simply add a "/" at the end of each "thread" value. This way, the
 * thread fields will look like this:
 *
 * 1/
 * 1.1/
 * 1.1.1/
 * 1.2/
 * 2/
 *
 * we add "/" since this char is, in ASCII, higher than every number, so if
 * now we "ORDER BY thread DESC" we get the correct order. However this would
 * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
 * to consider the trailing "/" so we use a substring only.
 */
707
function comment_get_thread(EntityInterface $entity, $field_name, $mode, $comments_per_page) {
708
709
  $query = db_select('comment', 'c')
    ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
710
711
  $query->addField('c', 'cid');
  $query
712
713
714
715
    ->condition('c.entity_id', $entity->id())
    ->condition('c.entity_type', $entity->entityType())
    ->condition('c.field_id', $entity->entityType() . '__' . $field_name)
    ->addTag('entity_access')
716
    ->addTag('comment_filter')
717
    ->addMetaData('base_table', 'comment')
718
719
    ->addMetaData('entity', $entity)
    ->addMetaData('field_name', $field_name)
720
721
722
723
724
    ->limit($comments_per_page);

  $count_query = db_select('comment', 'c');
  $count_query->addExpression('COUNT(*)');
  $count_query
725
726
727
728
    ->condition('c.entity_id', $entity->id())
    ->condition('c.entity_type', $entity->entityType())
    ->condition('c.field_id', $entity->entityType() . '__' . $field_name)
    ->addTag('entity_access')
729
    ->addTag('comment_filter')
730
    ->addMetaData('base_table', 'comment')
731
732
    ->addMetaData('entity', $entity)
    ->addMetaData('field_name', $field_name);
733
734
735
736
737

  if (!user_access('administer comments')) {
    $query->condition('c.status', COMMENT_PUBLISHED);
    $count_query->condition('c.status', COMMENT_PUBLISHED);
  }
738
  if ($mode == COMMENT_MODE_FLAT) {
739
740
741
742
743
744
    $query->orderBy('c.cid', 'ASC');
  }
  else {
    // See comment above. Analysis reveals that this doesn't cost too
    // much. It scales much much better than having the whole comment
    // structure.
745
746
    $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
    $query->orderBy('torder', 'ASC');
747
748
749
  }

  $query->setCountQuery($count_query);
750
  return $query->execute()->fetchCol();
751
752
753
}

/**
754
755
756
757
758
 * Calculates the indentation level of each comment in a comment thread.
 *
 * This function loops over an array representing a comment thread. For each
 * comment, the function calculates the indentation level and saves it in the
 * 'divs' property of the comment object.
759
760
 *
 * @param array $comments
761
 *   An array of comment objects, keyed by comment ID.
762
763
764
765
766
 */
function comment_prepare_thread(&$comments) {
  // A counter that helps track how indented we are.
  $divs = 0;

767
  foreach ($comments as $key => &$comment) {
768
769
    // The $divs element instructs #prefix whether to add an indent div or
    // close existing divs (a negative value).
770
    $comment->depth = count(explode('.', $comment->thread->value)) - 1;
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
    if ($comment->depth > $divs) {
      $comment->divs = 1;
      $divs++;
    }
    else {
      $comment->divs = $comment->depth - $divs;
      while ($comment->depth < $divs) {
        $divs--;
      }
    }
  }

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

/**
788
 * Generates an array for rendering a comment.
789
 *
790
 * @param \Drupal\comment\CommentInterface $comment
791
 *   The comment object.
792
 * @param $view_mode
793
 *   (optional) View mode, e.g. 'full', 'teaser'... Defaults to 'full'.
794
795
796
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
797
798
799
800
 *
 * @return
 *   An array as expected by drupal_render().
 */
801
function comment_view(CommentInterface $comment, $view_mode = 'full', $langcode = NULL) {
802
  return entity_view($comment, $view_mode, $langcode);
803
804
805
}

/**
806
 * Constructs render array from an array of loaded comments.
807
808
 *
 * @param $comments
809
 *   An array of comments as returned by entity_load_multiple().
810
811
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
812
 * @param $langcode
813
814
815
 *   (optional) A string indicating the language field values are to be shown
 *   in. If no language is provided the current content language is used.
 *   Defaults to NULL.
816
 *
817
818
 * @return
 *   An array in the format expected by drupal_render().
819
820
 *
 * @see drupal_render()
821
 */
822
823
function comment_view_multiple($comments, $view_mode = 'full', $langcode = NULL) {
  return entity_view_multiple($comments, $view_mode, $langcode);
824
825
}

826
/**
827
 * Implements hook_form_FORM_ID_alter().
828
 */
829
function comment_form_field_ui_field_instance_edit_form_alter(&$form, $form_state) {
830
  if ($form['#field']->getFieldType() == 'comment') {
831
832
    // Collect translation settings.
    if (\Drupal::moduleHandler()->moduleExists('content_translation')) {
833
834
      array_unshift($form['#submit'], 'comment_translation_configuration_element_submit');
    }
835
836
837

    // Hide required checkbox.
    $form['instance']['required']['#access'] = FALSE;
838
  }
839
840
}

841
/**
842
 * Implements hook_form_FORM_ID_alter().
843
 */
844
845
function comment_form_field_ui_field_overview_form_alter(&$form, $form_state) {
  if ($form['#entity_type'] == 'comment') {
846
847
    $request = \Drupal::request();
    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($request->attributes->get('commented_entity_type'), $request->attributes->get('field_name'));
848
  }
849
850
}

851
/**
852
 * Implements hook_form_FORM_ID_alter().
853
 */
854
855
function comment_form_field_ui_form_display_overview_form_alter(&$form, $form_state) {
  if ($form['#entity_type'] == 'comment') {
856
857
    $request = \Drupal::request();
    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($request->attributes->get('commented_entity_type'), $request->attributes->get('field_name'));
858
859
860
  }
}

Dries's avatar
   
Dries committed
861
/**
862
 * Implements hook_form_FORM_ID_alter().
Dries's avatar
   
Dries committed
863
 */
864
865
function comment_form_field_ui_display_overview_form_alter(&$form, $form_state) {
  if ($form['#entity_type'] == 'comment') {
866
867
    $request = \Drupal::request();
    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($request->attributes->get('commented_entity_type'), $request->attributes->get('field_name'));
868
  }
869
}
870

871
872
873
874
/**
 * Implements hook_form_FORM_ID_alter().
 */
function comment_form_field_ui_field_edit_form_alter(&$form, $form_state) {
875
  if ($form['#field']->getFieldType() == 'comment') {
876
877
878
879
    // We only support posting one comment at the time so it doesn't make sense
    // to let the site builder choose anything else.
    $form['field']['cardinality_container']['cardinality']['#options'] = drupal_map_assoc(array(1));
    $form['field']['cardinality_container']['#access'] = FALSE;
880
881
  }
}
882

883
/**
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
 * Form submission handler for field_ui_field_edit_form().
 *
 * This handles the comment translation settings added by
 * _comment_field_instance_settings_form_process().
 *
 * @see _comment_field_instance_settings_form_process()
 */
function comment_translation_configuration_element_submit($form, &$form_state) {
  // The comment translation settings form element is embedded into the instance
  // settings form. Hence we need to provide to the regular submit handler a
  // manipulated form state to make it process comment settings instead of the
  // host entity.
  $key = 'language_configuration';
  $comment_form_state = array(
    'content_translation' => array('key' => $key),
899
    'language' => array($key => array('entity_type' => 'comment', 'bundle' => $form['#field']['name'])),
900
901
902
903
904
905
906
907
    'values' => array($key => array('content_translation' => $form_state['values']['content_translation'])),
  );
  content_translation_language_configuration_element_submit($form, $comment_form_state);
}

/**
 * Implements hook_entity_load().
 *
908
 * @see \Drupal\comment\Plugin\Field\FieldType\CommentItem::getPropertyDefinitions()
909
 */
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
function comment_entity_load($entities, $entity_type) {
  if (!\Drupal::service('comment.manager')->getFields($entity_type)) {
    // Do not query database when entity has no comment fields.
    return;
  }
  // Load comment information from the database and update the entity's comment
  // statistics properties, which are defined on each CommentItem field.
  $result = db_select('comment_entity_statistics', 'ces')
    ->fields('ces')
    ->condition('ces.entity_id', array_keys($entities))
    ->condition('ces.entity_type', $entity_type)
    ->execute();
  foreach ($result as $record) {
    $parts = explode('__', $record->field_id, 2);
    list(, $field_name) = $parts;
    $comment_statistics = $entities[$record->entity_id]->get($field_name);
    $comment_statistics->cid = $record->cid;
    $comment_statistics->last_comment_timestamp = $record->last_comment_timestamp;
    $comment_statistics->last_comment_name = $record->last_comment_name;
    $comment_statistics->last_comment_uid = $record->last_comment_uid;
    $comment_statistics->comment_count = $record->comment_count;
931
932
  }
}
933

934
/**
935
 * Implements hook_entity_insert().
936
 */
937
function comment_entity_insert(EntityInterface $entity) {
938
  // Allow bulk updates and inserts to temporarily disable the
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
  // maintenance of the {comment_entity_statistics} table.
  if (\Drupal::state()->get('comment.maintain_entity_statistics') &&
    $fields = \Drupal::service('comment.manager')->getFields($entity->entityType())) {
    $query = db_insert('comment_entity_statistics')
     ->fields(array(
      'entity_id',
      'entity_type',
      'field_id',
      'cid',
      'last_comment_timestamp',
      'last_comment_name',
      'last_comment_uid',
      'comment_count'
    ));
    foreach ($fields as $field_name => $detail) {
      // Skip fields that entity does not have.
955
      if (!$entity->hasField($field_name)) {
956
957
958
959
        continue;
      }
      // There is at least one comment field, the query needs to be executed.
      // @todo Use $entity->getAuthorId() after https://drupal.org/node/2078387
960
      if ($entity->hasField('uid')) {
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
        $last_comment_uid = $entity->get('uid')->value;
      }
      else {
        // Default to current user when entity does not have a uid property.
        $last_comment_uid = \Drupal::currentUser()->id();
      }
      // Default to REQUEST_TIME when entity does not have a changed property.
      $last_comment_timestamp = REQUEST_TIME;
      if ($entity instanceof EntityChangedInterface) {
        $last_comment_timestamp = $entity->getChangedTime();
      }
      $query->values(array(
        'entity_id' => $entity->id(),
        'entity_type' => $entity->entityType(),
        'field_id' => $entity->entityType() . '__' . $field_name,
976
        'cid' => 0,
977
        'last_comment_timestamp' => $last_comment_timestamp,
978
        'last_comment_name' => NULL,
979
        'last_comment_uid' => $last_comment_uid,
980
        'comment_count' => 0,
981
982
983
      ));
    }
    $query->execute();
984
  }
985
}
986

987
/**
988
 * Implements hook_entity_predelete().
989
 */
990
991
992
993
994
995
996
function comment_entity_predelete(EntityInterface $entity) {
  $cids = db_select('comment', 'c')
    ->fields('c', array('cid'))
    ->condition('entity_id', $entity->id())
    ->condition('entity_type', $entity->entityType())
    ->execute()
    ->fetchCol();
997
  entity_delete_multiple('comment', $cids);
998
999
1000
  db_delete('comment_entity_statistics')
    ->condition('entity_id', $entity->id())
    ->condition('entity_type', $entity->entityType())
1001
    ->execute();
1002
}
1003

1004
/**
1005
 * Implements hook_node_update_index().
1006
 */
1007
function comment_node_update_index(EntityInterface $node, $langcode) {
1008
1009
1010
  $index_comments = &drupal_static(__FUNCTION__);

  if ($index_comments === NULL) {
1011
1012
1013
1014
1015
1016
1017
1018
1019
    // Do not index in the following three cases:
    // 1. 'Authenticated user' can search content but can't access comments.
    // 2. 'Anonymous user' can search content but can't access comments.
    // 3. Any role can search content but can't access comments and access
    // comments is not granted by the 'authenticated user' role. In this case
    // all users might have both permissions from various roles but it is also
    // possible to set up a user to have only search content and so a user
    // edit could change the security situation so it is not safe to index the
    // comments.
1020
    $index_comments = TRUE;
1021
    $roles = \Drupal::entityManager()->getStorageController('user_role')->loadMultiple();