comment.module 60.9 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
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\comment\CommentInterface;
16
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
17
18
19
use Drupal\field\FieldInstanceInterface;
use Drupal\field\FieldInterface;
use Drupal\file\FileInterface;
20

21
22
23
/**
 * Comments are displayed in a flat list - expanded.
 */
24
const COMMENT_MODE_FLAT = 0;
25
26
27
28

/**
 * Comments are displayed as a threaded list - expanded.
 */
29
const COMMENT_MODE_THREADED = 1;
Dries's avatar
Dries committed
30
31

/**
32
 * Anonymous posters cannot enter their contact information.
Dries's avatar
Dries committed
33
 */
34
const COMMENT_ANONYMOUS_MAYNOT_CONTACT = 0;
35
36
37
38

/**
 * Anonymous posters may leave their contact information.
 */
39
const COMMENT_ANONYMOUS_MAY_CONTACT = 1;
40
41

/**
42
 * Anonymous posters are required to leave their contact information.
43
 */
44
const COMMENT_ANONYMOUS_MUST_CONTACT = 2;
Dries's avatar
Dries committed
45
46

/**
47
 * Comment form should be displayed on a separate page.
Dries's avatar
Dries committed
48
 */
49
const COMMENT_FORM_SEPARATE_PAGE = 0;
50
51
52
53

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

/**
57
 * Comments for this entity are hidden.
Dries's avatar
Dries committed
58
 */
59
const COMMENT_HIDDEN = 0;
60
61

/**
62
 * Comments for this entity are closed.
63
 */
64
const COMMENT_CLOSED = 1;
65
66

/**
67
 * Comments for this entity are open.
68
 */
69
const COMMENT_OPEN = 2;
70

71
72
73
74
75
76
77
78
79
80
/**
 * 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);
81

82
/**
83
 * Implements hook_help().
84
 */
85
86
function comment_help($path, $arg) {
  switch ($path) {
Dries's avatar
   
Dries committed
87
    case 'admin/help#comment':
88
      $output = '<h3>' . t('About') . '</h3>';
89
      $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>';
90
91
92
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Default and custom settings') . '</dt>';
93
      $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>';
94
95
      $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>';
96
      $output .= '</dl>';
97
      return $output;
98
99
100
101

    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;
102
  }
Dries's avatar
   
Dries committed
103
104
}

105
106
107
108
109
/**
 * Implements hook_entity_bundle_info().
 */
function comment_entity_bundle_info() {
  $bundles = array();
110
111
112
113
114
115
116
117
118
  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'),
      );
    }
119
  }
120
  return $bundles;
121
122
}

123
/**
124
 * Entity URI callback.
125
 */
126
function comment_uri(CommentInterface $comment) {
127
  return array(
128
129
    'path' => 'comment/' . $comment->id(),
    'options' => array('fragment' => 'comment-' . $comment->id()),
130
  );
131
132
}

133
134
135
136
137
/**
 * Implements hook_field_extra_fields().
 */
function comment_field_extra_fields() {
  $return = array();
138
139
140
  foreach (\Drupal::service('comment.manager')->getAllFields() as $entity_type => $fields) {
    foreach ($fields as $field_name => $field_info) {
      $return['comment'][$entity_type . '__' . $field_name] = array(
141
142
143
144
145
146
        'form' => array(
          'author' => array(
            'label' => t('Author'),
            'description' => t('Author textfield'),
            'weight' => -2,
          ),
147
          'subject' => array(
148
149
150
151
            'label' => t('Subject'),
            'description' => t('Subject textfield'),
            'weight' => -1,
          ),
152
153
154
155
156
157
158
159
        ),
      );
    }
  }

  return $return;
}

160
/**
161
 * Implements hook_theme().
162
163
164
165
 */
function comment_theme() {
  return array(
    'comment_preview' => array(
166
      'variables' => array('comment' => NULL),
167
168
    ),
    'comment' => array(
169
      'template' => 'comment',
170
      'render element' => 'elements',
171
172
    ),
    'comment_post_forbidden' => array(
173
      'variables' => array('commented_entity' => NULL, 'field_name' => 'comment'),
174
175
    ),
    'comment_wrapper' => array(
176
      'template' => 'comment-wrapper',
177
      'render element' => 'content',
178
179
180
181
    ),
  );
}

Dries's avatar
   
Dries committed
182
/**
183
 * Implements hook_menu().
Dries's avatar
   
Dries committed
184
 */
185
function comment_menu() {
186
187
188
189
190
191
192
193
194
  $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',
  );
195
  $items['admin/content/comment'] = array(
196
    'title' => 'Comments',
197
    'description' => 'List and edit site comments and the comment approval queue.',
198
    'route_name' => 'comment.admin',
199
  );
Dries's avatar
   
Dries committed
200
201
202
203

  return $items;
}

204
205
206
207
/**
 * Implements hook_menu_alter().
 */
function comment_menu_alter(&$items) {
208
209
210
211
  if (isset($items['admin/content'])) {
    // Add comments to the description for admin/content if any.
    $items['admin/content']['description'] = 'Administer content and comments.';
  }
212
213
}

214
215
/**
 * Returns a menu title which includes the number of unapproved comments.
216
217
 *
 * @todo Move to the comment manager and replace by a entity query?
218
219
220
 */
function comment_count_unpublished() {
  $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE status = :status', array(
221
    ':status' => CommentInterface::NOT_PUBLISHED,
222
223
224
225
  ))->fetchField();
  return t('Unapproved comments (@count)', array('@count' => $count));
}

226
/**
227
228
229
 * Implements hook_ENTITY_TYPE_create() for 'field_instance'.
 */
function comment_field_instance_create(FieldInstanceInterface $instance) {
230
231
  if ($instance->getType() == 'comment' && !$instance->isSyncing()) {
    \Drupal::service('comment.manager')->addBodyField($instance->entity_type, $instance->getName());
232
233
234
235
236
237
238
239
240
241
242
    \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,
    ));
  }
243
}
244

245
/**
246
 * Implements hook_ENTITY_TYPE_update() for 'field_instance'.
247
 */
248
function comment_field_instance_update(FieldInstanceInterface $instance) {
249
  if ($instance->getType() == 'comment') {
250
    \Drupal::entityManager()->getViewBuilder($instance->entity_type)->resetCache();
251
252
253
    // Comment field settings also affects the rendering of *comment* entities,
    // not only the *commented* entities.
    \Drupal::entityManager()->getViewBuilder('comment')->resetCache();
254
255
  }
}
256

257
/**
258
 * Implements hook_ENTITY_TYPE_delete() for 'field_entity'.
259
 */
260
function comment_field_entity_delete(FieldInterface $field) {
261
  if ($field->getType() == 'comment') {
262
    // Delete all fields and displays attached to the comment bundle.
263
    entity_invoke_bundle_hook('delete', 'comment', $field->getName());
264
    \Drupal::cache()->delete('comment_entity_info');
265
266
267
  }
}

268
269
270
271
/**
 * Implements hook_ENTITY_TYPE_delete() for 'field_instance'.
 */
function comment_field_instance_delete(FieldInstanceInterface $instance) {
272
  if ($instance->getType() == 'comment') {
273
274
275
    // 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(),
276
      ':field_id' => $instance->entityType() . '__' . $instance->getName(),
277
278
279
    ))->fetchCol();
    entity_delete_multiple('comment', $comments);
    \Drupal::cache()->delete('comment_entity_info');
280
  }
281
282
}

Dries's avatar
   
Dries committed
283
/**
284
 * Implements hook_permission().
Dries's avatar
   
Dries committed
285
 */
286
function comment_permission() {
287
  return array(
288
    'administer comments' => array(
289
      'title' => t('Administer comments and comment settings'),
290
291
    ),
    'access comments' => array(
292
      'title' => t('View comments'),
293
294
    ),
    'post comments' => array(
295
      'title' => t('Post comments'),
296
    ),
297
298
    'skip comment approval' => array(
      'title' => t('Skip comment approval'),
299
    ),
300
301
302
    'edit own comments' => array(
      'title' => t('Edit own comments'),
    ),
303
  );
Dries's avatar
   
Dries committed
304
305
}

306
/**
307
 * Finds the most recent comments that are available to the current user.
308
 *
309
 * @param int $number
310
 *   (optional) The maximum number of comments to find. Defaults to 10.
311
 *
312
 * @return \Drupal\comment\CommentInterface[]|array
313
314
 *   An array of comment objects or an empty array if there are no recent
 *   comments visible to the current user.
315
316
 */
function comment_get_recent($number = 10) {
317
  $query = db_select('comment', 'c');
318
  $query->addMetaData('base_table', 'comment');
319
  $query->fields('c')
320
    ->condition('c.status', CommentInterface::PUBLISHED);
321
322
323
324
  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');
325
326
    // @todo This should be actually filtering on the desired node status field
    //   language and just fall back to the default language.
327
328
329
330
331
    $query
      ->condition('n.status', NODE_PUBLISHED)
      ->condition('n.default_langcode', 1);
  }
  $comments = $query
332
    ->orderBy('c.created', 'DESC')
333
334
335
    // Additionally order by cid to ensure that comments with the same timestamp
    // are returned in the exact order posted.
    ->orderBy('c.cid', 'DESC')
336
337
338
339
340
    ->range(0, $number)
    ->execute()
    ->fetchAll();

  return $comments ? $comments : array();
341
342
}

343
/**
344
 * Calculates the page number for the first new comment.
345
 *
346
 * @param int $num_comments
347
 *   Number of comments.
348
 * @param int $new_replies
349
 *   Number of new replies.
350
351
352
353
 * @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.
354
 *
355
356
 * @return array|NULL
 *   An array "page=X" if the page number is greater than zero; NULL otherwise.
357
 */
358
359
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);
360
361
  $mode = $instance->getSetting('default_mode');
  $comments_per_page = $instance->getSetting('per_page');
362
  $pagenum = NULL;
363
  $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
364
365
  if ($num_comments <= $comments_per_page) {
    // Only one page of comments.
366
    $pageno = 0;
367
  }
368
369
370
  elseif ($flat) {
    // Flat comments.
    $count = $num_comments - $new_replies;
371
    $pageno = $count / $comments_per_page;
372
  }
373
  else {
374
375
376
377
378
379
    // 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'))
380
381
382
      ->condition('entity_id', $entity->id())
      ->condition('entity_type', $entity->entityType())
      ->condition('field_id', $entity->entityType() . '__' . $field_name)
383
      ->condition('status', CommentInterface::PUBLISHED)
384
385
      ->orderBy('created', 'DESC')
      ->orderBy('cid', 'DESC')
386
387
388
389
390
391
392
393
394
395
396
397
398
399
      ->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.
400
401
402
403
    $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(
404
      ':status' => CommentInterface::PUBLISHED,
405
406
407
      ':entity_id' => $entity->id(),
      ':field_id' => $entity->entityType() . '__' . $field_name,
      ':entity_type' => $entity->entityType(),
408
      ':thread' => $first_thread,
409
    ))->fetchField();
410

411
    $pageno = $count / $comments_per_page;
412
  }
413

414
  if ($pageno >= 1) {
415
    $pagenum = array('page' => intval($pageno));
416
  }
417

418
419
420
  return $pagenum;
}

421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
/**
 * Implements hook_entity_view_alter().
 */
function comment_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
  // Add the comment page number to the cache key if render caching is enabled.
  if (isset($build['#cache']) && isset($build['#cache']['keys']) && \Drupal::request()->query->has('page')) {
    foreach ($entity->getPropertyDefinitions() as $field_name => $definition) {
      if (isset($build[$field_name]) && $definition->getType() === 'comment') {
        $display_options = $display->getComponent($field_name);
        $pager_id = $display_options['settings']['pager_id'];
        $page = pager_find_page($pager_id);
        $build['#cache']['keys'][] = $field_name . '-pager-' . $page;
      }
    }
  }
}

Dries's avatar
   
Dries committed
438
/**
439
 * Implements hook_entity_view().
Dries's avatar
   
Dries committed
440
 */
441
function comment_entity_view(EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode, $langcode) {
442
443
444
445
446
447
448
449
450
451
452
  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.
453
    if (!$entity->hasField($field_name)) {
454
      continue;
455
    }
456
457
458
459
460
461
462
463
464
465
466
    $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
467
        }
468
469
470
471
        $entity->rss_elements[] = array(
          'key' => 'comments',
          'value' => url($uri['path'], $uri['options'])
        );
472
      }
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
      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,
              );
            }
499
          }
500
        }
501
502
        // Provide a link to new comment form.
        if ($commenting_status == COMMENT_OPEN) {
503
          $comment_form_location = $instance->getSetting('form_location');
504
          if (user_access('post comments')) {
505
506
            $links['comment-add'] = array(
              'title' => t('Add new comment'),
507
508
              'href' => $uri['path'],
              'attributes' => array('title' => t('Add a new comment to this page.')),
509
510
511
              'fragment' => 'comment-form',
            );
            if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
512
              $links['comment-add']['href'] = 'comment/reply/'. $entity->entityType() . '/' . $entity->id() .'/' . $field_name;
513
            }
514
          }
515
516
517
518
519
520
521
522
523
524
525
          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
526
        }
527
528
529
530
531
532
533
      }
      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) {
534
          $comment_form_location = $instance->getSetting('form_location');
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
          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
561
562
563
        }
      }
    }
Dries's avatar
Dries committed
564

565
566
    $entity->content['links']['comment__' . $field_name] = array(
      '#theme' => 'links__entity__comment__' . $field_name,
567
568
569
      '#links' => $links,
      '#attributes' => array('class' => array('links', 'inline')),
    );
570
    if ($view_mode == 'teaser' && \Drupal::moduleHandler()->moduleExists('history') && \Drupal::currentUser()->isAuthenticated()) {
571
      $entity->content['links']['#attached']['library'][] = array('comment', 'drupal.node-new-comments-link');
572
    }
573
  }
Dries's avatar
   
Dries committed
574
575
}

576
577
578
/**
 * Implements hook_node_view_alter().
 */
579
function comment_node_view_alter(&$build, EntityInterface $node, EntityViewDisplayInterface $display) {
580
  if (\Drupal::moduleHandler()->moduleExists('history')) {
581
582
583
584
    $build['#attributes']['data-history-node-id'] = $node->id();
  }
}

585
/**
586
 * Returns a rendered form to comment the given entity.
587
 *
588
589
590
591
 * @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.
592
593
594
 * @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.
595
596
597
598
 *
 * @return array
 *   The renderable array for the comment addition form.
 */
599
600
601
602
603
604
605
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,
  );
606
  $comment = entity_create('comment', $values);
607
  return \Drupal::entityManager()->getForm($comment);
608
609
}

610
/**
611
 * Retrieves comments for a thread.
612
 *
613
614
615
616
617
 * @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
618
 *   The comment display mode; COMMENT_MODE_FLAT or COMMENT_MODE_THREADED.
619
 * @param int $comments_per_page
620
 *   The amount of comments to display per page.
621
622
623
 * @param int $pager_id
 *   (optional) Pager id to use in case of multiple pagers on the one page.
 *   Defaults to 0.
624
 *
625
 * @return int[]
626
627
 *   An array of the IDs of the comment to be displayed.
 *
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
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
 * 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.
 */
682
function comment_get_thread(EntityInterface $entity, $field_name, $mode, $comments_per_page, $pager_id = 0) {
683
684
  $query = db_select('comment', 'c')
    ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
685
686
687
  if ($pager_id) {
    $query->element($pager_id);
  }
688
689
  $query->addField('c', 'cid');
  $query
690
691
692
693
    ->condition('c.entity_id', $entity->id())
    ->condition('c.entity_type', $entity->entityType())
    ->condition('c.field_id', $entity->entityType() . '__' . $field_name)
    ->addTag('entity_access')
694
    ->addTag('comment_filter')
695
    ->addMetaData('base_table', 'comment')
696
697
    ->addMetaData('entity', $entity)
    ->addMetaData('field_name', $field_name)
698
699
700
701
702
    ->limit($comments_per_page);

  $count_query = db_select('comment', 'c');
  $count_query->addExpression('COUNT(*)');
  $count_query
703
704
705
706
    ->condition('c.entity_id', $entity->id())
    ->condition('c.entity_type', $entity->entityType())
    ->condition('c.field_id', $entity->entityType() . '__' . $field_name)
    ->addTag('entity_access')
707
    ->addTag('comment_filter')
708
    ->addMetaData('base_table', 'comment')
709
710
    ->addMetaData('entity', $entity)
    ->addMetaData('field_name', $field_name);
711
712

  if (!user_access('administer comments')) {
713
714
    $query->condition('c.status', CommentInterface::PUBLISHED);
    $count_query->condition('c.status', CommentInterface::PUBLISHED);
715
  }
716
  if ($mode == COMMENT_MODE_FLAT) {
717
718
719
720
721
722
    $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.
723
724
    $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
    $query->orderBy('torder', 'ASC');
725
726
727
  }

  $query->setCountQuery($count_query);
728
  return $query->execute()->fetchCol();
729
730
731
}

/**
732
733
734
735
736
 * 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.
737
738
 *
 * @param array $comments
739
 *   An array of comment objects, keyed by comment ID.
740
741
742
743
744
 */
function comment_prepare_thread(&$comments) {
  // A counter that helps track how indented we are.
  $divs = 0;

745
  foreach ($comments as $key => &$comment) {
746
747
    // The $divs element instructs #prefix whether to add an indent div or
    // close existing divs (a negative value).
748
    $comment->depth = count(explode('.', $comment->thread->value)) - 1;
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
    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;
}

/**
766
 * Generates an array for rendering a comment.
767
 *
768
 * @param \Drupal\comment\CommentInterface $comment
769
 *   The comment object.
770
 * @param $view_mode
771
 *   (optional) View mode, e.g. 'full', 'teaser'... Defaults to 'full'.
772
773
774
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
775
 *
776
 * @return array
777
778
 *   An array as expected by drupal_render().
 */
779
function comment_view(CommentInterface $comment, $view_mode = 'full', $langcode = NULL) {
780
  return entity_view($comment, $view_mode, $langcode);
781
782
783
}

/**
784
 * Constructs render array from an array of loaded comments.
785
786
 *
 * @param $comments
787
 *   An array of comments as returned by entity_load_multiple().
788
789
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
790
 * @param $langcode
791
792
793
 *   (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.
794
 *
795
 * @return array
796
 *   An array in the format expected by drupal_render().
797
798
 *
 * @see drupal_render()
799
 */
800
801
function comment_view_multiple($comments, $view_mode = 'full', $langcode = NULL) {
  return entity_view_multiple($comments, $view_mode, $langcode);
802
803
}

804
/**
805
 * Implements hook_form_FORM_ID_alter().
806
 */
807
function comment_form_field_ui_field_instance_edit_form_alter(&$form, $form_state) {
808
  if ($form['#field']->getType() == 'comment') {
809
810
    // Collect translation settings.
    if (\Drupal::moduleHandler()->moduleExists('content_translation')) {
811
812
      array_unshift($form['#submit'], 'comment_translation_configuration_element_submit');
    }
813
814
815

    // Hide required checkbox.
    $form['instance']['required']['#access'] = FALSE;
816
  }
817
818
}

819
/**
820
 * Implements hook_form_FORM_ID_alter().
821
 */
822
function comment_form_field_ui_field_overview_form_alter(&$form, $form_state) {
823
824
  $request = \Drupal::request();
  if ($form['#entity_type'] == 'comment' && $request->attributes->has('commented_entity_type')) {
825
    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($request->attributes->get('commented_entity_type'), $request->attributes->get('field_name'));
826
  }
827
828
}

829
/**
830
 * Implements hook_form_FORM_ID_alter().
831
 */
832
function comment_form_field_ui_form_display_overview_form_alter(&$form, $form_state) {
833
834
  $request = \Drupal::request();
  if ($form['#entity_type'] == 'comment' && $request->attributes->has('commented_entity_type')) {
835
    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($request->attributes->get('commented_entity_type'), $request->attributes->get('field_name'));
836
837
838
  }
}

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

849
850
851
852
/**
 * Implements hook_form_FORM_ID_alter().
 */
function comment_form_field_ui_field_edit_form_alter(&$form, $form_state) {
853
  if ($form['#field']->getType() == 'comment') {
854
855
856
857
    // 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;
858
859
  }
}
860

861
/**
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
 * 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),
877
    'language' => array($key => array('entity_type' => 'comment', 'bundle' => $form['#field']->name)),
878
879
880
881
882
883
884
885
    '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().
 *
886
 * @see \Drupal\comment\Plugin\Field\FieldType\CommentItem::getPropertyDefinitions()
887
 */
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
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;
903
904
905
906
907

    // Skip fields that entity does not have.
    if (!$entities[$record->entity_id]->hasField($field_name)) {
      continue;
    }
908
909
910
911
912
913
    $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;
914
915
  }
}
916

917
/**
918
 * Implements hook_entity_insert().
919
 */
920
function comment_entity_insert(EntityInterface $entity) {
921
  // Allow bulk updates and inserts to temporarily disable the
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
  // 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.
938
      if (!$entity->hasField($field_name)) {
939
940
941
942
        continue;
      }
      // There is at least one comment field, the query needs to be executed.
      // @todo Use $entity->getAuthorId() after https://drupal.org/node/2078387
943
      if ($entity->hasField('uid')) {
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
        $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,
959
        'cid' => 0,
960
        'last_comment_timestamp' => $last_comment_timestamp,
961
        'last_comment_name' => NULL,
962
        'last_comment_uid' => $last_comment_uid,
963
        'comment_count' => 0,
964
965
966
      ));
    }
    $query->execute();
967
  }
968
}
969

970
/**
971
 * Implements hook_entity_predelete().
972
 */
973
974
975
976
977
978
979
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();
980
  entity_delete_multiple('comment', $cids);
981
982
983
  db_delete('comment_entity_statistics')
    ->condition('entity_id', $entity->id())
    ->condition('entity_type', $entity->entityType())
984
    ->execute();
985
}
986

987
/**
988
 * Implements hook_node_update_index().
989
 */
990
function comment_node_update_index(EntityInterface $node, $langcode) {