comment.module 53.3 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\comment\CommentInterface;
14
use Drupal\comment\Entity\Comment;
15
use Drupal\comment\Entity\CommentType;
16
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
17
use Drupal\Component\Utility\String;
18
use Drupal\Core\Entity\ContentEntityInterface;
19
use Drupal\Core\Entity\EntityInterface;
20
use Drupal\entity\Entity\EntityViewDisplay;
21
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
22
use Drupal\Core\Field\FieldDefinitionInterface;
23
use Drupal\Core\Render\Element;
24
use Drupal\Core\Url;
25
use Drupal\field\Entity\FieldConfig;
26 27
use Drupal\field\FieldInstanceConfigInterface;
use Drupal\field\FieldConfigInterface;
28
use Drupal\file\FileInterface;
29
use Drupal\user\EntityOwnerInterface;
30
use Drupal\node\NodeInterface;
31
use Symfony\Component\HttpFoundation\Request;
32

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

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

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

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

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

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

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

68 69 70 71 72 73 74 75 76 77
/**
 * 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);
78

79
/**
80
 * Implements hook_help().
81
 */
82 83 84
function comment_help($route_name, Request $request) {
  switch ($route_name) {
    case 'help.page.comment':
85
      $output = '<h3>' . t('About') . '</h3>';
86
      $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 <a href="!comment">online documentation for the Comment module</a>.', array('!comment' => 'https://drupal.org/documentation/modules/comment')) . '</p>';
87 88
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
89 90 91 92 93 94
      $output .= '<dt>' . t('Enabling commenting and configuring defaults') . '</dt>';
      $output .= '<dd>' . t('Comment functionality can be enabled for any <a href="!entity-help" title="Entity module help">entity sub-type</a> (for example, a <a href="!content-type">content type</a>). On the Manage fields page for each entity sub-type, you can enable commenting by adding a Comments field. The entity sub-types each have their own default comment settings configured as: <em>Open</em> to allow new comments, <em>Closed</em> to view existing comments, but prevent new comments, or <em>Hidden</em> to hide existing comments and prevent new comments.', array('!content-type' => \Drupal::url('node.overview_types'), '!entity-help' => \Drupal::url('help.page', array('name' => 'entity')))) . '</dd>';
      $output .= '<dt>' . t('Overriding default settings') . '</dt>';
      $output .= '<dd>' . t('When you create an entity item, you can override the default comment settings. Changing the entity sub-type defaults will not affect existing entity items, whether they used the default settings or had overrides.') . '</dd>';
      $output .= '<dt>' . t('Approving and managing comments') . '</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 and comment settings</em> publishes or deletes them. Published comments can be bulk managed on the <a href="!admin-comment">Published comments</a> administration page. When a comment has no replies, it remains editable by its author, as long as the author has <em>Edit own comments</em> permission.', array('!comment-approval' => \Drupal::url('comment.admin_approval'), '!admin-comment' => \Drupal::url('comment.admin'))) . '</dd>';
95
      $output .= '</dl>';
96
      return $output;
97

98 99
    case 'comment.type_list':
      $output = '<p>' . t('This page provides a list of all comment types on the site and allows you to manage the fields, form and display settings for each.') . '</p>';
100
      return $output;
101
  }
Dries's avatar
 
Dries committed
102 103
}

104
/**
105
 * Entity URI callback.
106
 */
107
function comment_uri(CommentInterface $comment) {
108 109 110
  return new Url(
    'comment.permalink',
    array(
111 112
      'comment' => $comment->id(),
    ),
113
    array('fragment' => 'comment-' . $comment->id())
114
  );
115 116
}

117
/**
118
 * Implements hook_entity_extra_field_info().
119
 */
120
function comment_entity_extra_field_info() {
121
  $return = array();
122
  foreach (CommentType::loadMultiple() as $comment_type) {
123
    $return['comment'][$comment_type->id()] = array(
124 125 126 127 128
      'form' => array(
        'author' => array(
          'label' => t('Author'),
          'description' => t('Author textfield'),
          'weight' => -2,
129
        ),
130 131 132 133 134 135 136
        'subject' => array(
          'label' => t('Subject'),
          'description' => t('Subject textfield'),
          'weight' => -1,
        ),
      ),
    );
137 138 139 140 141
  }

  return $return;
}

142
/**
143
 * Implements hook_theme().
144 145 146 147
 */
function comment_theme() {
  return array(
    'comment' => array(
148
      'render element' => 'elements',
149
      'template' => 'comment',
150 151
    ),
    'comment_wrapper' => array(
152
      'render element' => 'content',
153
      'template' => 'comment-wrapper',
154 155 156 157
    ),
  );
}

158 159
/**
 * Returns a menu title which includes the number of unapproved comments.
160 161
 *
 * @todo Move to the comment manager and replace by a entity query?
162 163 164
 */
function comment_count_unpublished() {
  $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE status = :status', array(
165
    ':status' => CommentInterface::NOT_PUBLISHED,
166 167 168 169
  ))->fetchField();
  return t('Unapproved comments (@count)', array('@count' => $count));
}

170
/**
171
 * Implements hook_ENTITY_TYPE_create() for 'field_instance_config'.
172
 */
173
function comment_field_instance_config_create(FieldInstanceConfigInterface $instance) {
174
  if ($instance->getType() == 'comment' && !$instance->isSyncing()) {
175
    // Assign default values for the field instance.
176 177 178 179 180
    if (!isset($instance->default_value)) {
      $instance->default_value = array();
    }
    $instance->default_value += array(array());
    $instance->default_value[0] += array(
181
      'status' => CommentItemInterface::OPEN,
182 183 184 185 186
      'cid' => 0,
      'last_comment_timestamp' => 0,
      'last_comment_name' => '',
      'last_comment_uid' => 0,
      'comment_count' => 0,
187
    );
188
  }
189
}
190

191
/**
192
 * Implements hook_ENTITY_TYPE_update() for 'field_instance_config'.
193
 */
194
function comment_field_instance_config_update(FieldInstanceConfigInterface $instance) {
195
  if ($instance->getType() == 'comment') {
196 197 198
    // Comment field settings also affects the rendering of *comment* entities,
    // not only the *commented* entities.
    \Drupal::entityManager()->getViewBuilder('comment')->resetCache();
199 200
  }
}
201

202 203 204 205 206
/**
 * Implements hook_ENTITY_TYPE_insert() for 'field_config'.
 */
function comment_field_config_insert(FieldConfigInterface $field) {
  if ($field->getType() == 'comment') {
207 208 209 210 211
    // Check that the target entity type uses an integer ID.
    $entity_type_id = $field->getTargetEntityTypeId();
    if (!_comment_entity_uses_integer_id($entity_type_id)) {
      throw new \UnexpectedValueException('You cannot attach a comment field to an entity with a non-integer ID field');
    }
212 213 214
  }
}

215
/**
216
 * Implements hook_ENTITY_TYPE_delete() for 'field_instance_config'.
217
 */
218
function comment_field_instance_config_delete(FieldInstanceConfigInterface $instance) {
219
  if ($instance->getType() == 'comment') {
220
    // Delete all comments that used by the entity bundle.
221
    $comments = db_query("SELECT cid FROM {comment} WHERE entity_type = :entity_type AND field_name = :field_name", array(
222
      ':entity_type' => $instance->getEntityTypeId(),
223
      ':field_name' => $instance->getName(),
224 225
    ))->fetchCol();
    entity_delete_multiple('comment', $comments);
226
  }
227 228
}

Dries's avatar
 
Dries committed
229
/**
230
 * Implements hook_permission().
Dries's avatar
 
Dries committed
231
 */
232
function comment_permission() {
233
  return array(
234
    'administer comments' => array(
235
      'title' => t('Administer comments and comment settings'),
236
    ),
237 238 239 240
    'administer comment types' => array(
      'title' => t('Administer comment types and settings'),
      'restrict access' => TRUE,
    ),
241
    'access comments' => array(
242
      'title' => t('View comments'),
243 244
    ),
    'post comments' => array(
245
      'title' => t('Post comments'),
246
    ),
247 248
    'skip comment approval' => array(
      'title' => t('Skip comment approval'),
249
    ),
250 251 252
    'edit own comments' => array(
      'title' => t('Edit own comments'),
    ),
253
  );
Dries's avatar
 
Dries committed
254 255
}

256
/**
257
 * Calculates the page number for the first new comment.
258
 *
259
 * @param int $num_comments
260
 *   Number of comments.
261
 * @param int $new_replies
262
 *   Number of new replies.
263
 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
264 265 266
 *   The first new comment entity.
 * @param string $field_name
 *   The field name on the entity to which comments are attached to.
267
 *
268
 * @return array|null
269
 *   An array "page=X" if the page number is greater than zero; NULL otherwise.
270
 */
271 272 273 274
function comment_new_page_count($num_comments, $new_replies, ContentEntityInterface $entity, $field_name = 'comment') {
  $field_definition = $entity->getFieldDefinition($field_name);
  $mode = $field_definition->getSetting('default_mode');
  $comments_per_page = $field_definition->getSetting('per_page');
275
  $pagenum = NULL;
276
  $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
277 278
  if ($num_comments <= $comments_per_page) {
    // Only one page of comments.
279
    $pageno = 0;
280
  }
281 282 283
  elseif ($flat) {
    // Flat comments.
    $count = $num_comments - $new_replies;
284
    $pageno = $count / $comments_per_page;
285
  }
286
  else {
287 288 289 290 291 292
    // 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'))
293
      ->condition('entity_id', $entity->id())
294
      ->condition('entity_type', $entity->getEntityTypeId())
295
      ->condition('field_name', $field_name)
296
      ->condition('status', CommentInterface::PUBLISHED)
297 298
      ->orderBy('created', 'DESC')
      ->orderBy('cid', 'DESC')
299 300 301
      ->range(0, $new_replies);

    // 2. Find the first thread.
302 303 304
    $first_thread_query = db_select($unread_threads_query, 'thread');
    $first_thread_query->addExpression('SUBSTRING(thread, 1, (LENGTH(thread) - 1))', 'torder');
    $first_thread = $first_thread_query
305
      ->fields('thread', array('thread'))
306
      ->orderBy('torder')
307 308 309 310 311 312 313 314
      ->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.
315 316
    $count = db_query('SELECT COUNT(*) FROM {comment} WHERE entity_id = :entity_id
                      AND entity_type = :entity_type
317
                      AND field_name = :field_name
318
                      AND status = :status AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread', array(
319
      ':status' => CommentInterface::PUBLISHED,
320
      ':entity_id' => $entity->id(),
321
      ':field_name' => $field_name,
322
      ':entity_type' => $entity->getEntityTypeId(),
323
      ':thread' => $first_thread,
324
    ))->fetchField();
325

326
    $pageno = $count / $comments_per_page;
327
  }
328

329
  if ($pageno >= 1) {
330
    $pagenum = array('page' => intval($pageno));
331
  }
332

333 334 335
  return $pagenum;
}

336
/**
337
 * Implements hook_entity_build_defaults_alter().
338
 */
339 340 341
function comment_entity_build_defaults_alter(array &$build, EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
  // Get the corresponding display settings.
  $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
342 343
  // 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')) {
344
    foreach ($entity->getFieldDefinitions() as $field_name => $definition) {
345
      if ($definition->getType() === 'comment' && ($display_options = $display->getComponent($field_name))) {
346 347 348 349 350 351
        $pager_id = $display_options['settings']['pager_id'];
        $page = pager_find_page($pager_id);
        $build['#cache']['keys'][] = $field_name . '-pager-' . $page;
      }
    }
  }
352
  return $build;
353 354
}

Dries's avatar
 
Dries committed
355
/**
356
 * Implements hook_node_links_alter().
Dries's avatar
 
Dries committed
357
 */
358 359 360 361 362 363 364 365
function comment_node_links_alter(array &$node_links, NodeInterface $node, array &$context) {
  // 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

  $view_mode = $context['view_mode'];
366
  if ($view_mode == 'search_index' || $view_mode == 'search_result' || $view_mode == 'print') {
367
    // Do not add any links if the node displayed for:
368 369 370 371 372
    // - search indexing.
    // - constructing a search result excerpt.
    // - print.
    return;
  }
373

374 375
  $fields = \Drupal::service('comment.manager')->getFields('node');
  foreach ($fields as $field_name => $detail) {
376 377
    // Skip fields that the node does not have.
    if (!$node->hasField($field_name)) {
378
      continue;
379
    }
380
    $links = array();
381
    $commenting_status = $node->get($field_name)->status;
382
    if ($commenting_status) {
383
      $field_definition = $node->getFieldDefinition($field_name);
384
      // Node have commenting open or close.
385 386
      if ($view_mode == 'rss') {
        // Add a comments RSS element which is a URL to the comments of this node.
387 388 389 390
        $options = array(
          'fragment' => 'comments',
          'absolute' => TRUE,
        );
391
        $node->rss_elements[] = array(
392
          'key' => 'comments',
393
          'value' => $node->url('canonical', $options),
394
        );
395
      }
396 397 398 399 400
      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')) {
401
          if (!empty($node->get($field_name)->comment_count)) {
402
            $links['comment-comments'] = array(
403
              'title' => format_plural($node->get($field_name)->comment_count, '1 comment', '@count comments'),
404 405 406
              'attributes' => array('title' => t('Jump to the first comment of this posting.')),
              'fragment' => 'comments',
              'html' => TRUE,
407
            ) + $node->urlInfo()->toArray();
408 409 410 411 412 413 414
            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.'),
415
                  'data-history-node-last-comment-timestamp' => $node->get($field_name)->last_comment_timestamp,
416 417 418 419 420
                  'data-history-node-field-name' => $field_name,
                ),
                'html' => TRUE,
              );
            }
421
          }
422
        }
423
        // Provide a link to new comment form.
424
        if ($commenting_status == CommentItemInterface::OPEN) {
425
          $comment_form_location = $field_definition->getSetting('form_location');
426
          if (user_access('post comments')) {
427 428
            $links['comment-add'] = array(
              'title' => t('Add new comment'),
429
              'language' => $node->language(),
430
              'attributes' => array('title' => t('Add a new comment to this page.')),
431 432 433
              'fragment' => 'comment-form',
            );
            if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
434 435
              $links['comment-add']['route_name'] = 'comment.reply';
              $links['comment-add']['route_parameters'] = array(
436 437
                'entity_type' => $node->getEntityTypeId(),
                'entity_id' => $node->id(),
438 439 440 441
                'field_name' => $field_name,
              );
            }
            else {
442
              $links['comment-add'] += $node->urlInfo()->toArray();
443
            }
444
          }
445
          elseif (\Drupal::currentUser()->isAnonymous()) {
446
            $links['comment-forbidden'] = array(
447
              'title' => \Drupal::service('comment.manager')->forbiddenMessage($node, $field_name),
448 449 450
              'html' => TRUE,
            );
          }
Dries's avatar
 
Dries committed
451
        }
452
      }
453
      else {
454 455
        // Node in other view modes: add a "post comment" link if the user is
        // allowed to post comments and if this node is allowing new comments.
456
        if ($commenting_status == CommentItemInterface::OPEN) {
457
          $comment_form_location = $field_definition->getSetting('form_location');
458 459 460
          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.
461
            if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE || (!empty($node->get($field_name)->comment_count) && user_access('access comments'))) {
462 463 464 465 466 467
              $links['comment-add'] = array(
                'title' => t('Add new comment'),
                'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
                'fragment' => 'comment-form',
              );
              if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
468 469
                $links['comment-add']['route_name'] = 'comment.reply';
                $links['comment-add']['route_parameters'] = array(
470 471
                  'entity_type' => $node->getEntityTypeId(),
                  'entity_id' => $node->id(),
472 473 474 475
                  'field_name' => $field_name,
                );
              }
              else {
476
                $links['comment-add'] += $node->urlInfo()->toArray();
477 478 479
              }
            }
          }
480
          elseif (\Drupal::currentUser()->isAnonymous()) {
481
            $links['comment-forbidden'] = array(
482
              'title' => \Drupal::service('comment.manager')->forbiddenMessage($node, $field_name),
483 484 485
              'html' => TRUE,
            );
          }
Dries's avatar
 
Dries committed
486 487 488
        }
      }
    }
Dries's avatar
Dries committed
489

490 491 492 493 494
    if (!empty($links)) {
      $node_links['comment__' . $field_name] = array(
        '#theme' => 'links__entity__comment__' . $field_name,
        '#links' => $links,
        '#attributes' => array('class' => array('links', 'inline')),
495
      );
496
      if ($view_mode == 'teaser' && \Drupal::moduleHandler()->moduleExists('history') && \Drupal::currentUser()->isAuthenticated()) {
497
        $node_links['comment__' . $field_name]['#attached']['library'][] = 'comment/drupal.node-new-comments-link';
498 499 500 501 502 503 504 505 506

        // Embed the metadata for the "X new comments" link (if any) on this node.
        $node_links['comment__' . $field_name]['#post_render_cache']['history_attach_timestamp'] = array(
          array('node_id' => $node->id()),
        );
        $node_links['comment__' . $field_name]['#post_render_cache']['Drupal\comment\CommentViewBuilder::attachNewCommentsLinkMetadata'] = array(
          array('entity_type' => $node->getEntityTypeId(), 'entity_id' => $node->id(), 'field_name' => $field_name),
        );
      }
507
    }
508
  }
Dries's avatar
 
Dries committed
509 510
}

511 512 513
/**
 * Implements hook_node_view_alter().
 */
514
function comment_node_view_alter(array &$build, EntityInterface $node, EntityViewDisplayInterface $display) {
515
  if (\Drupal::moduleHandler()->moduleExists('history')) {
516 517 518 519
    $build['#attributes']['data-history-node-id'] = $node->id();
  }
}

520
/**
521
 * Returns a rendered form to comment the given entity.
522
 *
523 524 525 526
 * @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.
527 528 529
 * @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.
530 531 532 533
 *
 * @return array
 *   The renderable array for the comment addition form.
 */
534
function comment_add(EntityInterface $entity, $field_name = 'comment', $pid = NULL) {
535
  $field = Fieldconfig::loadByName($entity->getEntityTypeId(), $field_name);
536
  $values = array(
537
    'entity_type' => $entity->getEntityTypeId(),
538
    'entity_id' => $entity->id(),
539 540
    'field_name' => $field_name,
    'comment_type' => $field->getSetting('bundle'),
541 542
    'pid' => $pid,
  );
543
  $comment = entity_create('comment', $values);
544
  return \Drupal::service('entity.form_builder')->getForm($comment);
545 546
}

547
/**
548
 * Retrieves comments for a thread.
549
 *
550 551 552 553 554
 * @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
555
 *   The comment display mode; COMMENT_MODE_FLAT or COMMENT_MODE_THREADED.
556
 * @param int $comments_per_page
557
 *   The amount of comments to display per page.
558 559 560
 * @param int $pager_id
 *   (optional) Pager id to use in case of multiple pagers on the one page.
 *   Defaults to 0.
561
 *
562
 * @return int[]
563 564
 *   An array of the IDs of the comment to be displayed.
 *
565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
 * 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.
 */
619
function comment_get_thread(EntityInterface $entity, $field_name, $mode, $comments_per_page, $pager_id = 0) {
620 621
  $query = db_select('comment', 'c')
    ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
622 623 624
  if ($pager_id) {
    $query->element($pager_id);
  }
625 626
  $query->addField('c', 'cid');
  $query
627
    ->condition('c.entity_id', $entity->id())
628
    ->condition('c.entity_type', $entity->getEntityTypeId())
629
    ->condition('c.field_name', $field_name)
630
    ->addTag('entity_access')
631
    ->addTag('comment_filter')
632
    ->addMetaData('base_table', 'comment')
633 634
    ->addMetaData('entity', $entity)
    ->addMetaData('field_name', $field_name)
635 636 637 638 639
    ->limit($comments_per_page);

  $count_query = db_select('comment', 'c');
  $count_query->addExpression('COUNT(*)');
  $count_query
640
    ->condition('c.entity_id', $entity->id())
641
    ->condition('c.entity_type', $entity->getEntityTypeId())
642
    ->condition('c.field_name', $field_name)
643
    ->addTag('entity_access')
644
    ->addTag('comment_filter')
645
    ->addMetaData('base_table', 'comment')
646 647
    ->addMetaData('entity', $entity)
    ->addMetaData('field_name', $field_name);
648 649

  if (!user_access('administer comments')) {
650 651
    $query->condition('c.status', CommentInterface::PUBLISHED);
    $count_query->condition('c.status', CommentInterface::PUBLISHED);
652
  }
653
  if ($mode == COMMENT_MODE_FLAT) {
654 655 656 657 658 659
    $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.
660 661
    $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
    $query->orderBy('torder', 'ASC');
662 663 664
  }

  $query->setCountQuery($count_query);
665
  return $query->execute()->fetchCol();
666 667 668
}

/**
669 670 671 672 673
 * 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.
674 675
 *
 * @param array $comments
676
 *   An array of comment objects, keyed by comment ID.
677 678 679 680 681
 */
function comment_prepare_thread(&$comments) {
  // A counter that helps track how indented we are.
  $divs = 0;

682
  foreach ($comments as $key => &$comment) {
683 684
    // The $divs element instructs #prefix whether to add an indent div or
    // close existing divs (a negative value).
685
    $comment->depth = count(explode('.', $comment->getThread())) - 1;
686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702
    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;
}

/**
703
 * Generates an array for rendering a comment.
704
 *
705
 * @param \Drupal\comment\CommentInterface $comment
706
 *   The comment object.
707
 * @param $view_mode
708
 *   (optional) View mode, e.g. 'full', 'teaser'... Defaults to 'full'.
709 710 711
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
712
 *
713
 * @return array
714 715
 *   An array as expected by drupal_render().
 */
716
function comment_view(CommentInterface $comment, $view_mode = 'full', $langcode = NULL) {
717
  return entity_view($comment, $view_mode, $langcode);
718 719 720
}

/**
721
 * Constructs render array from an array of loaded comments.
722 723
 *
 * @param $comments
724
 *   An array of comments as returned by entity_load_multiple().
725 726
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
727
 * @param $langcode
728 729 730
 *   (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.
731
 *
732
 * @return array
733
 *   An array in the format expected by drupal_render().
734 735
 *
 * @see drupal_render()
736
 */
737 738
function comment_view_multiple($comments, $view_mode = 'full', $langcode = NULL) {
  return entity_view_multiple($comments, $view_mode, $langcode);
739 740
}

741
/**
742
 * Implements hook_form_FORM_ID_alter().
743
 */
744
function comment_form_field_ui_field_overview_form_alter(&$form, $form_state) {
745 746
  $request = \Drupal::request();
  if ($form['#entity_type'] == 'comment' && $request->attributes->has('commented_entity_type')) {
747
    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($request->attributes->get('commented_entity_type'), $request->attributes->get('field_name'));
748
  }
749 750 751 752 753
  $entity_type_id = $form['#entity_type'];
  if (!_comment_entity_uses_integer_id($entity_type_id)) {
    // You cannot use comment fields on entity types with non-integer IDs.
    unset($form['fields']['_add_new_field']['type']['#options']['comment']);
  }
754 755
}

756
/**
757
 * Implements hook_form_FORM_ID_alter().
758
 */
759
function comment_form_field_ui_form_display_overview_form_alter(&$form, $form_state) {
760 761
  $request = \Drupal::request();
  if ($form['#entity_type'] == 'comment' && $request->attributes->has('commented_entity_type')) {
762
    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($request->attributes->get('commented_entity_type'), $request->attributes->get('field_name'));
763 764 765
  }
}

Dries's avatar
 
Dries committed
766
/**
767
 * Implements hook_form_FORM_ID_alter().
Dries's avatar
 
Dries committed
768
 */
alexpott's avatar