first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
/**
* @file
* Attaches comment behaviors to the entity form.
*/
(function ($, Drupal) {
/**
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.commentFieldsetSummaries = {
attach(context) {
const $context = $(context);
$context
.find('fieldset.comment-entity-settings-form')
.drupalSetSummary((context) =>
Drupal.checkPlain(
$(context)
.find('.js-form-item-comment input:checked')
.next('label')[0].textContent,
),
);
},
};
})(jQuery, Drupal);

View File

@@ -0,0 +1,48 @@
<?php
/**
* @file
* Hooks provided by the Comment module.
*/
use Drupal\comment\CommentInterface;
use Drupal\Core\Url;
/**
* @addtogroup hooks
* @{
*/
/**
* Alter the links of a comment.
*
* @param array &$links
* A renderable array representing the comment links.
* @param \Drupal\comment\CommentInterface $entity
* The comment being rendered.
* @param array &$context
* Various aspects of the context in which the comment links are going to be
* displayed, with the following keys:
* - 'view_mode': the view mode in which the comment is being viewed
* - 'langcode': the language in which the comment is being viewed
* - 'commented_entity': the entity to which the comment is attached
*
* @see \Drupal\comment\CommentViewBuilder::renderLinks()
* @see \Drupal\comment\CommentViewBuilder::buildLinks()
*/
function hook_comment_links_alter(array &$links, CommentInterface $entity, array &$context) {
$links['my_module'] = [
'#theme' => 'links__comment__my_module',
'#attributes' => ['class' => ['links', 'inline']],
'#links' => [
'comment-report' => [
'title' => t('Report'),
'url' => Url::fromRoute('comment_test.report', ['comment' => $entity->id()], ['query' => ['token' => \Drupal::getContainer()->get('csrf_token')->get("comment/{$entity->id()}/report")]]),
],
],
];
}
/**
* @} End of "addtogroup hooks".
*/

View File

@@ -0,0 +1,13 @@
name: Comment
type: module
description: 'Allows users to comment on content.'
package: Core
# version: VERSION
dependencies:
- drupal:text
configure: comment.admin
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,136 @@
<?php
/**
* @file
* Install, update and uninstall functions for the Comment module.
*/
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Implements hook_uninstall().
*/
function comment_uninstall() {
// Remove the comment fields.
$storage = \Drupal::entityTypeManager()->getStorage('field_storage_config');
$fields = $storage->loadByProperties(['type' => 'comment']);
$storage->delete($fields);
// Remove state setting.
\Drupal::state()->delete('comment.node_comment_statistics_scale');
}
/**
* Implements hook_install().
*/
function comment_install() {
// By default, maintain entity statistics for comments.
// @see \Drupal\comment\CommentStatisticsInterface
\Drupal::state()->set('comment.maintain_entity_statistics', TRUE);
}
/**
* Implements hook_schema().
*/
function comment_schema() {
$schema['comment_entity_statistics'] = [
'description' => 'Maintains statistics of entity and comments posts to show "new" and "updated" flags.',
'fields' => [
'entity_id' => [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'The entity_id of the entity for which the statistics are compiled.',
],
'entity_type' => [
'type' => 'varchar_ascii',
'not null' => TRUE,
'default' => 'node',
'length' => EntityTypeInterface::ID_MAX_LENGTH,
'description' => 'The entity_type of the entity to which this comment is a reply.',
],
'field_name' => [
'type' => 'varchar_ascii',
'not null' => TRUE,
'default' => '',
'length' => FieldStorageConfig::NAME_MAX_LENGTH,
'description' => 'The field_name of the field that was used to add this comment.',
],
'cid' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The {comment}.cid of the last comment.',
],
'last_comment_timestamp' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.changed.',
'size' => 'big',
],
'last_comment_name' => [
'type' => 'varchar',
'length' => 60,
'not null' => FALSE,
'description' => 'The name of the latest author to post a comment on this node, from {comment}.name.',
],
'last_comment_uid' => [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'The user ID of the latest author to post a comment on this node, from {comment}.uid.',
],
'comment_count' => [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'The total number of comments on this entity.',
],
],
'primary key' => ['entity_id', 'entity_type', 'field_name'],
'indexes' => [
'last_comment_timestamp' => ['last_comment_timestamp'],
'comment_count' => ['comment_count'],
'last_comment_uid' => ['last_comment_uid'],
],
'foreign keys' => [
'last_comment_author' => [
'table' => 'users',
'columns' => [
'last_comment_uid' => 'uid',
],
],
],
];
return $schema;
}
/**
* Implements hook_update_last_removed().
*/
function comment_update_last_removed() {
return 8701;
}
/**
* Remove the year 2038 date limitation.
*/
function comment_update_10100(&$sandbox = NULL) {
$connection = \Drupal::database();
if ($connection->schema()->tableExists('comment_entity_statistics') && $connection->databaseType() != 'sqlite') {
$new = [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.changed.',
'size' => 'big',
];
$connection->schema()->changeField('comment_entity_statistics', 'last_comment_timestamp', 'last_comment_timestamp', $new);
}
}

View File

@@ -0,0 +1,46 @@
drupal.comment:
version: VERSION
js:
comment-entity-form.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupal.form
drupal.comment-by-viewer:
version: VERSION
js:
js/comment-by-viewer.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupalSettings
drupal.comment-new-indicator:
version: VERSION
js:
js/comment-new-indicator.js: {}
dependencies:
- core/jquery
- core/once
- core/drupal
- history/api
- core/drupal.displace
drupal.node-new-comments-link:
version: VERSION
js:
js/node-new-comments-link.js: {}
dependencies:
- core/jquery
- core/once
- core/drupal
- history/api
drupal.comment-icon:
version: VERSION
css:
theme:
css/comment.icon.theme.css: {}
dependencies:
- field_ui/drupal.field_ui.manage_fields

View File

@@ -0,0 +1,5 @@
comment_type_add:
route_name: entity.comment_type.add_form
title: 'Add comment type'
appears_on:
- entity.comment_type.collection

View File

@@ -0,0 +1,10 @@
comment.admin:
title: Comments
route_name: comment.admin
parent: system.admin_content
description: 'List and edit site comments and the comment approval queue.'
entity.comment_type.collection:
title: 'Comment types'
route_name: entity.comment_type.collection
parent: system.admin_structure
description: 'Manage form and displays settings of comments.'

View File

@@ -0,0 +1,39 @@
entity.comment.canonical_tab:
route_name: entity.comment.canonical
title: 'View comment'
base_route: entity.comment.canonical
entity.comment.edit_form_tab:
route_name: entity.comment.edit_form
title: 'Edit'
base_route: entity.comment.canonical
weight: 0
entity.comment.delete_form_tab:
route_name: entity.comment.delete_form
title: 'Delete'
base_route: entity.comment.canonical
weight: 10
comment.admin:
title: Comments
route_name: comment.admin
base_route: system.admin_content
comment.admin_new:
title: 'Published comments'
route_name: comment.admin
parent_id: comment.admin
comment.admin_approval:
title: 'Unapproved comments'
route_name: comment.admin_approval
class: Drupal\comment\Plugin\Menu\LocalTask\UnapprovedComments
parent_id: comment.admin
weight: 1
cache_tags:
- comment_list
# Default tab for comment type editing.
entity.comment_type.edit_form:
title: 'Edit'
route_name: entity.comment_type.edit_form
base_route: entity.comment_type.edit_form

View File

@@ -0,0 +1,786 @@
<?php
/**
* @file
* Enables users to comment on published content.
*
* When installed, 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 story, user etc.
*/
use Drupal\comment\CommentInterface;
use Drupal\comment\Entity\CommentType;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\field\FieldConfigInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\node\NodeInterface;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
/**
* 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 https://www.drupal.org/node/2006632 lands.
*/
define('COMMENT_NEW_LIMIT', ((int) $_SERVER['REQUEST_TIME']) - 30 * 24 * 60 * 60);
/**
* Implements hook_help().
*/
function comment_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.comment':
$output = '<h2>' . t('About') . '</h2>';
$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>.', [':comment' => 'https://www.drupal.org/documentation/modules/comment']) . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . t('Enabling commenting') . '</dt>';
$output .= '<dd>' . t('Comment functionality can be enabled for any entity sub-type (for example, a <a href=":content-type">content type</a>) by adding a <em>Comments</em> field on its <em>Manage fields page</em>. Adding or removing commenting for an entity through the user interface requires the <a href=":field_ui">Field UI</a> module to be installed, even though the commenting functionality works without it. For more information on fields and entities, see the <a href=":field">Field module help page</a>.', [':content-type' => (\Drupal::moduleHandler()->moduleExists('node')) ? Url::fromRoute('entity.node_type.collection')->toString() : '#', ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>';
$output .= '<dt>' . t('Configuring commenting settings') . '</dt>';
$output .= '<dd>' . t('Commenting settings can be configured by editing the <em>Comments</em> field on the <em>Manage fields page</em> of an entity type if the <em>Field UI module</em> is installed. Configuration includes the label of the comments field, the number of comments to be displayed, and whether they are shown in threaded list. Commenting can be 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. Changing this configuration for an entity type will not change existing entity items.') . '</dd>';
$output .= '<dt>' . t('Overriding default settings') . '</dt>';
$output .= '<dd>' . t('Users with the appropriate permissions can override the default commenting settings of an entity type when they create an item of that type.') . '</dd>';
$output .= '<dt>' . t('Adding comment types') . '</dt>';
$output .= '<dd>' . t('Additional <em>comment types</em> can be created per entity sub-type and added on the <a href=":field">Comment types page</a>. If there are multiple comment types available you can select the appropriate one after adding a <em>Comments field</em>.', [':field' => Url::fromRoute('entity.comment_type.collection')->toString()]) . '</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.', [':comment-approval' => Url::fromRoute('comment.admin_approval')->toString(), ':admin-comment' => Url::fromRoute('comment.admin')->toString()]) . '</dd>';
$output .= '</dl>';
return $output;
case 'entity.comment_type.collection':
$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>';
return $output;
}
}
/**
* Entity URI callback.
*/
function comment_uri(CommentInterface $comment) {
return new Url(
'entity.comment.canonical',
[
'comment' => $comment->id(),
],
['fragment' => 'comment-' . $comment->id()]
);
}
/**
* Implements hook_entity_extra_field_info().
*/
function comment_entity_extra_field_info() {
$return = [];
foreach (CommentType::loadMultiple() as $comment_type) {
$return['comment'][$comment_type->id()] = [
'form' => [
'author' => [
'label' => t('Author'),
'description' => t('Author textfield'),
'weight' => -2,
],
],
];
$return['comment'][$comment_type->id()]['display']['links'] = [
'label' => t('Links'),
'description' => t('Comment operation links'),
'weight' => 100,
'visible' => TRUE,
];
}
return $return;
}
/**
* Implements hook_theme().
*/
function comment_theme() {
return [
'comment' => [
'render element' => 'elements',
],
'field__comment' => [
'base hook' => 'field',
],
];
}
/**
* Implements hook_ENTITY_TYPE_create() for 'field_config'.
*/
function comment_field_config_create(FieldConfigInterface $field) {
if ($field->getType() == 'comment' && !$field->isSyncing()) {
// Assign default values for the field.
$default_value = $field->getDefaultValueLiteral();
$default_value += [[]];
$default_value[0] += [
'status' => CommentItemInterface::OPEN,
'cid' => 0,
'last_comment_timestamp' => 0,
'last_comment_name' => '',
'last_comment_uid' => 0,
'comment_count' => 0,
];
$field->setDefaultValue($default_value);
}
}
/**
* Implements hook_ENTITY_TYPE_update() for 'field_config'.
*/
function comment_field_config_update(FieldConfigInterface $field) {
if ($field->getType() == 'comment') {
// Comment field settings also affects the rendering of *comment* entities,
// not only the *commented* entities.
\Drupal::entityTypeManager()->getViewBuilder('comment')->resetCache();
}
}
/**
* Implements hook_ENTITY_TYPE_insert() for 'field_storage_config'.
*/
function comment_field_storage_config_insert(FieldStorageConfigInterface $field_storage) {
if ($field_storage->getType() == 'comment') {
// Check that the target entity type uses an integer ID.
$entity_type_id = $field_storage->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');
}
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for 'field_config'.
*/
function comment_field_config_delete(FieldConfigInterface $field) {
if ($field->getType() == 'comment') {
// Delete all comments that used by the entity bundle.
$entity_query = \Drupal::entityQuery('comment')->accessCheck(FALSE);
$entity_query->condition('entity_type', $field->getEntityTypeId());
$entity_query->condition('field_name', $field->getName());
$cids = $entity_query->execute();
$comment_storage = \Drupal::entityTypeManager()->getStorage('comment');
$comments = $comment_storage->loadMultiple($cids);
$comment_storage->delete($comments);
}
}
/**
* Implements hook_node_links_alter().
*/
function comment_node_links_alter(array &$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
// https://www.drupal.org/node/1901110.
$comment_links = \Drupal::service('comment.link_builder')->buildCommentedEntityLinks($node, $context);
$links += $comment_links;
}
/**
* Implements hook_entity_view().
*/
function comment_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
if ($entity instanceof FieldableEntityInterface && $view_mode == 'rss' && $display->getComponent('links')) {
/** @var \Drupal\comment\CommentManagerInterface $comment_manager */
$comment_manager = \Drupal::service('comment.manager');
$fields = $comment_manager->getFields($entity->getEntityTypeId());
foreach ($fields as $field_name => $detail) {
if ($entity->hasField($field_name) && $entity->get($field_name)->status != CommentItemInterface::HIDDEN) {
// Add a comments RSS element which is a URL to the comments of this
// entity.
$options = [
'fragment' => 'comments',
'absolute' => TRUE,
];
$entity->rss_elements[] = [
'key' => 'comments',
'value' => $entity->toUrl('canonical', $options)->toString(),
];
}
}
}
}
/**
* Implements hook_ENTITY_TYPE_view_alter() for node entities.
*/
function comment_node_view_alter(array &$build, EntityInterface $node, EntityViewDisplayInterface $display) {
if (\Drupal::moduleHandler()->moduleExists('history')) {
$build['#attributes']['data-history-node-id'] = $node->id();
}
}
/**
* Implements hook_form_FORM_ID_alter() for field_ui_field_storage_add_form.
*/
function comment_form_field_ui_field_storage_add_form_alter(&$form, FormStateInterface $form_state) {
$route_match = \Drupal::routeMatch();
if ($form_state->get('entity_type_id') == 'comment' && $route_match->getParameter('commented_entity_type')) {
$form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name'));
}
}
/**
* Implements hook_field_info_entity_type_ui_definitions_alter().
*/
function comment_field_info_entity_type_ui_definitions_alter(array &$ui_definitions, string $entity_type_id) {
if (!_comment_entity_uses_integer_id($entity_type_id)) {
unset($ui_definitions['comment']);
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function comment_form_field_ui_form_display_overview_form_alter(&$form, FormStateInterface $form_state) {
$route_match = \Drupal::routeMatch();
if ($form['#entity_type'] == 'comment' && $route_match->getParameter('commented_entity_type')) {
$form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name'));
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function comment_form_field_ui_display_overview_form_alter(&$form, FormStateInterface $form_state) {
$route_match = \Drupal::routeMatch();
if ($form['#entity_type'] == 'comment' && $route_match->getParameter('commented_entity_type')) {
$form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name'));
}
}
/**
* Implements hook_entity_storage_load().
*
* @see \Drupal\comment\Plugin\Field\FieldType\CommentItem::propertyDefinitions()
*/
function comment_entity_storage_load($entities, $entity_type) {
// Comments can only be attached to content entities, so skip others.
if (!\Drupal::entityTypeManager()->getDefinition($entity_type)->entityClassImplements(FieldableEntityInterface::class)) {
return;
}
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 = \Drupal::service('comment.statistics')->read($entities, $entity_type);
foreach ($result as $record) {
// Skip fields that entity does not have.
if (!$entities[$record->entity_id]->hasField($record->field_name)) {
continue;
}
$comment_statistics = $entities[$record->entity_id]->get($record->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;
}
}
/**
* Implements hook_entity_insert().
*/
function comment_entity_insert(EntityInterface $entity) {
// Allow bulk updates and inserts to temporarily disable the
// maintenance of the {comment_entity_statistics} table.
if (\Drupal::state()->get('comment.maintain_entity_statistics') &&
$fields = \Drupal::service('comment.manager')->getFields($entity->getEntityTypeId())) {
\Drupal::service('comment.statistics')->create($entity, $fields);
}
}
/**
* Implements hook_entity_predelete().
*/
function comment_entity_predelete(EntityInterface $entity) {
// Entities can have non-numeric IDs, but {comment} and
// {comment_entity_statistics} tables have integer columns for entity ID, and
// PostgreSQL throws exceptions if you attempt query conditions with
// mismatched types. So, we need to verify that the ID is numeric (even for an
// entity type that has an integer ID, $entity->id() might be a string
// containing a number), and then cast it to an integer when querying.
if ($entity instanceof FieldableEntityInterface && is_numeric($entity->id())) {
$entity_query = \Drupal::entityQuery('comment')->accessCheck(FALSE);
$entity_query->condition('entity_id', (int) $entity->id());
$entity_query->condition('entity_type', $entity->getEntityTypeId());
$cids = $entity_query->execute();
$comment_storage = \Drupal::entityTypeManager()->getStorage('comment');
$comments = $comment_storage->loadMultiple($cids);
$comment_storage->delete($comments);
\Drupal::service('comment.statistics')->delete($entity);
}
}
/**
* Determines if an entity type is using an integer-based ID definition.
*
* @param string $entity_type_id
* The ID the represents the entity type.
*
* @return bool
* Returns TRUE if the entity type has an integer-based ID definition and
* FALSE otherwise.
*/
function _comment_entity_uses_integer_id($entity_type_id) {
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
$entity_type_id_key = $entity_type->getKey('id');
if ($entity_type_id_key === FALSE) {
return FALSE;
}
$field_definitions = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($entity_type->id());
$entity_type_id_definition = $field_definitions[$entity_type_id_key];
return $entity_type_id_definition->getType() === 'integer';
}
/**
* Implements hook_node_update_index().
*/
function comment_node_update_index(EntityInterface $node) {
$index_comments = &drupal_static(__FUNCTION__);
if ($index_comments === NULL) {
// 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.
$index_comments = TRUE;
$roles = \Drupal::entityTypeManager()->getStorage('user_role')->loadMultiple();
$authenticated_can_access = $roles[RoleInterface::AUTHENTICATED_ID]->hasPermission('access comments');
foreach ($roles as $rid => $role) {
if ($role->hasPermission('search content') && !$role->hasPermission('access comments')) {
if ($rid == RoleInterface::AUTHENTICATED_ID || $rid == RoleInterface::ANONYMOUS_ID || !$authenticated_can_access) {
$index_comments = FALSE;
break;
}
}
}
}
$build = [];
if ($index_comments) {
foreach (\Drupal::service('comment.manager')->getFields('node') as $field_name => $info) {
// Skip fields that entity does not have.
if (!$node->hasField($field_name)) {
continue;
}
$field_definition = $node->getFieldDefinition($field_name);
$mode = $field_definition->getSetting('default_mode');
$comments_per_page = $field_definition->getSetting('per_page');
if ($node->get($field_name)->status) {
$comments = \Drupal::entityTypeManager()->getStorage('comment')
->loadThread($node, $field_name, $mode, $comments_per_page);
if ($comments) {
$build[] = \Drupal::entityTypeManager()->getViewBuilder('comment')->viewMultiple($comments);
}
}
}
}
return \Drupal::service('renderer')->renderInIsolation($build);
}
/**
* Implements hook_cron().
*/
function comment_cron() {
// Store the maximum possible comments per thread (used for node search
// ranking by reply count).
\Drupal::state()->set('comment.node_comment_statistics_scale', 1.0 / max(1, \Drupal::service('comment.statistics')->getMaximumCount('node')));
}
/**
* Implements hook_node_search_result().
*
* Formats a comment count string and returns it, for display with search
* results.
*/
function comment_node_search_result(EntityInterface $node) {
$comment_fields = \Drupal::service('comment.manager')->getFields('node');
$comments = 0;
$open = FALSE;
foreach ($comment_fields as $field_name => $info) {
// Skip fields that entity does not have.
if (!$node->hasField($field_name)) {
continue;
}
// Do not make a string if comments are hidden.
$status = $node->get($field_name)->status;
if (\Drupal::currentUser()->hasPermission('access comments') && $status != CommentItemInterface::HIDDEN) {
if ($status == CommentItemInterface::OPEN) {
// At least one comment field is open.
$open = TRUE;
}
$comments += $node->get($field_name)->comment_count;
}
}
// Do not make a string if there are no comment fields, or no comments exist
// or all comment fields are hidden.
if ($comments > 0 || $open) {
return ['comment' => \Drupal::translation()->formatPlural($comments, '1 comment', '@count comments')];
}
}
/**
* Implements hook_user_cancel().
*/
function comment_user_cancel($edit, UserInterface $account, $method) {
switch ($method) {
case 'user_cancel_block_unpublish':
$comments = \Drupal::entityTypeManager()->getStorage('comment')->loadByProperties(['uid' => $account->id()]);
foreach ($comments as $comment) {
$comment->setUnpublished();
$comment->save();
}
break;
case 'user_cancel_reassign':
/** @var \Drupal\comment\CommentInterface[] $comments */
$comments = \Drupal::entityTypeManager()->getStorage('comment')->loadByProperties(['uid' => $account->id()]);
foreach ($comments as $comment) {
$langcodes = array_keys($comment->getTranslationLanguages());
// For efficiency manually save the original comment before applying any
// changes.
$comment->original = clone $comment;
foreach ($langcodes as $langcode) {
$comment_translated = $comment->getTranslation($langcode);
$comment_translated->setOwnerId(0);
$comment_translated->setAuthorName(\Drupal::config('user.settings')->get('anonymous'));
}
$comment->save();
}
break;
}
}
/**
* Implements hook_ENTITY_TYPE_predelete() for user entities.
*/
function comment_user_predelete($account) {
$entity_query = \Drupal::entityQuery('comment')->accessCheck(FALSE);
$entity_query->condition('uid', $account->id());
$cids = $entity_query->execute();
$comment_storage = \Drupal::entityTypeManager()->getStorage('comment');
$comments = $comment_storage->loadMultiple($cids);
$comment_storage->delete($comments);
}
/**
* Generates a comment preview.
*
* @param \Drupal\comment\CommentInterface $comment
* The comment entity to preview.
* @param Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* An array as expected by \Drupal\Core\Render\RendererInterface::render().
*/
function comment_preview(CommentInterface $comment, FormStateInterface $form_state) {
$preview_build = [];
$entity = $comment->getCommentedEntity();
if (!$form_state->getErrors()) {
$comment->in_preview = TRUE;
$comment_build = \Drupal::entityTypeManager()->getViewBuilder('comment')->view($comment);
$comment_build['#weight'] = -100;
$preview_build['comment_preview'] = $comment_build;
}
if ($comment->hasParentComment()) {
$build = [];
$parent = $comment->getParentComment();
if ($parent && $parent->isPublished()) {
$build = \Drupal::entityTypeManager()->getViewBuilder('comment')->view($parent);
}
}
else {
// The comment field output includes rendering the parent entity of the
// thread to which the comment is a reply. The rendered entity output
// includes the comment reply form, which contains the comment preview and
// therefore the rendered parent entity. This results in an infinite loop of
// parent entity output rendering the comment form and the comment form
// rendering the parent entity. To prevent this infinite loop we temporarily
// set the value of the comment field on a clone of the entity to hidden
// before calling the entity view builder. That way when the output of
// the commented entity is rendered, it excludes the comment field output.
$field_name = $comment->getFieldName();
$entity = clone $entity;
$entity->$field_name->status = CommentItemInterface::HIDDEN;
$build = \Drupal::entityTypeManager()
->getViewBuilder($entity->getEntityTypeId())
->view($entity, 'full');
}
$preview_build['comment_output_below'] = $build;
$preview_build['comment_output_below']['#weight'] = 200;
return $preview_build;
}
/**
* Implements hook_preprocess_HOOK() for block templates.
*/
function comment_preprocess_block(&$variables) {
if ($variables['configuration']['provider'] == 'comment') {
$variables['attributes']['role'] = 'navigation';
}
}
/**
* Prepares variables for comment templates.
*
* By default this function performs special preprocessing of some base fields
* so they are available as variables in the template. For example 'subject'
* appears as 'title'. This preprocessing is skipped if:
* - a module makes the field's display configurable via the field UI by means
* of BaseFieldDefinition::setDisplayConfigurable()
* - AND the additional entity type property
* 'enable_base_field_custom_preprocess_skipping' has been set using
* hook_entity_type_build().
*
* Default template: comment.html.twig.
*
* @param array $variables
* An associative array containing:
* - elements: An associative array containing the comment and entity objects.
* Array keys: #comment, #commented_entity.
*/
function template_preprocess_comment(&$variables) {
/** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
$date_formatter = \Drupal::service('date.formatter');
/** @var \Drupal\comment\CommentInterface $comment */
$comment = $variables['elements']['#comment'];
$commented_entity = $comment->getCommentedEntity();
$variables['comment'] = $comment;
$variables['commented_entity'] = $commented_entity;
$variables['threaded'] = $variables['elements']['#comment_threaded'];
$skip_custom_preprocessing = $comment->getEntityType()->get('enable_base_field_custom_preprocess_skipping');
// Make created, uid, pid and subject fields available separately. Skip this
// custom preprocessing if the field display is configurable and skipping has
// been enabled.
// @todo https://www.drupal.org/project/drupal/issues/3015623
// Eventually delete this code and matching template lines. Using
// $variables['content'] is more flexible and consistent.
$submitted_configurable = $comment->getFieldDefinition('created')->isDisplayConfigurable('view') || $comment->getFieldDefinition('uid')->isDisplayConfigurable('view');
if (!$skip_custom_preprocessing || !$submitted_configurable) {
$account = $comment->getOwner();
$username = [
'#theme' => 'username',
'#account' => $account,
];
$variables['author'] = \Drupal::service('renderer')->render($username);
$variables['author_id'] = $comment->getOwnerId();
$variables['new_indicator_timestamp'] = $comment->getChangedTime();
$variables['created'] = $date_formatter->format($comment->getCreatedTime());
// Avoid calling DateFormatterInterface::format() twice on the same timestamp.
if ($comment->getChangedTime() == $comment->getCreatedTime()) {
$variables['changed'] = $variables['created'];
}
else {
$variables['changed'] = $date_formatter->format($comment->getChangedTime());
}
if (theme_get_setting('features.comment_user_picture')) {
// To change user picture settings (for instance, image style), edit the
// 'compact' view mode on the User entity.
$variables['user_picture'] = \Drupal::entityTypeManager()
->getViewBuilder('user')
->view($account, 'compact');
}
else {
$variables['user_picture'] = [];
}
$variables['submitted'] = t('Submitted by @username on @datetime', ['@username' => $variables['author'], '@datetime' => $variables['created']]);
}
if (isset($comment->in_preview)) {
$variables['permalink'] = Link::fromTextAndUrl(t('Permalink'), Url::fromRoute('<front>'))->toString();
}
else {
$variables['permalink'] = Link::fromTextAndUrl(t('Permalink'), $comment->permalink())->toString();
}
if (($comment_parent = $comment->getParentComment()) && (!$skip_custom_preprocessing || !$comment->getFieldDefinition('pid')->isDisplayConfigurable('view'))) {
// Fetch and store the parent comment information for use in templates.
$account_parent = $comment_parent->getOwner();
$variables['parent_comment'] = $comment_parent;
$username = [
'#theme' => 'username',
'#account' => $account_parent,
];
$variables['parent_author'] = \Drupal::service('renderer')->render($username);
$variables['parent_created'] = $date_formatter->format($comment_parent->getCreatedTime());
// Avoid calling DateFormatterInterface::format() twice on same timestamp.
if ($comment_parent->getChangedTime() == $comment_parent->getCreatedTime()) {
$variables['parent_changed'] = $variables['parent_created'];
}
else {
$variables['parent_changed'] = $date_formatter->format($comment_parent->getChangedTime());
}
$permalink_uri_parent = $comment_parent->permalink();
$attributes = $permalink_uri_parent->getOption('attributes') ?: [];
$attributes += ['class' => ['permalink'], 'rel' => 'bookmark'];
$permalink_uri_parent->setOption('attributes', $attributes);
$variables['parent_title'] = Link::fromTextAndUrl($comment_parent->getSubject(), $permalink_uri_parent)->toString();
$variables['parent_permalink'] = Link::fromTextAndUrl(t('Parent permalink'), $permalink_uri_parent)->toString();
$variables['parent'] = t('In reply to @parent_title by @parent_username',
['@parent_username' => $variables['parent_author'], '@parent_title' => $variables['parent_title']]);
}
else {
$variables['parent_comment'] = '';
$variables['parent_author'] = '';
$variables['parent_created'] = '';
$variables['parent_changed'] = '';
$variables['parent_title'] = '';
$variables['parent_permalink'] = '';
$variables['parent'] = '';
}
if (!$skip_custom_preprocessing || !$comment->getFieldDefinition('subject')->isDisplayConfigurable('view')) {
if (isset($comment->in_preview)) {
$variables['title'] = Link::fromTextAndUrl($comment->getSubject(), Url::fromRoute('<front>'))->toString();
}
else {
$uri = $comment->permalink();
$attributes = $uri->getOption('attributes') ?: [];
$attributes += ['class' => ['permalink'], 'rel' => 'bookmark'];
$uri->setOption('attributes', $attributes);
$variables['title'] = Link::fromTextAndUrl($comment->getSubject(), $uri)->toString();
}
}
// Helpful $content variable for templates.
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
// Set status to a string representation of comment->status.
if (isset($comment->in_preview)) {
$variables['status'] = 'preview';
}
else {
$variables['status'] = $comment->isPublished() ? 'published' : 'unpublished';
}
// Add comment author user ID. Necessary for the comment-by-viewer library.
$variables['attributes']['data-comment-user-id'] = $comment->getOwnerId();
// Add anchor for each comment.
$variables['attributes']['id'] = 'comment-' . $comment->id();
}
/**
* Prepares variables for comment field templates.
*
* Default template: field--comment.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing render arrays for the list of
* comments, and the comment form. Array keys: comments, comment_form.
*
* @todo Rename to template_preprocess_field__comment() once
* https://www.drupal.org/node/939462 is resolved.
*/
function comment_preprocess_field(&$variables) {
$element = $variables['element'];
if ($element['#field_type'] == 'comment') {
// Provide contextual information.
$variables['comment_display_mode'] = $element[0]['#comment_display_mode'];
$variables['comment_type'] = $element[0]['#comment_type'];
// Append additional attributes from the first field item.
$variables['attributes'] += $variables['items'][0]['attributes']->storage();
// Create separate variables for the comments and comment form.
$variables['comments'] = $element[0]['comments'];
$variables['comment_form'] = $element[0]['comment_form'];
}
}
/**
* Implements hook_ranking().
*/
function comment_ranking() {
return \Drupal::service('comment.statistics')->getRankingInfo();
}
/**
* Implements hook_ENTITY_TYPE_presave() for entity_view_display entities.
*/
function comment_entity_view_display_presave(EntityViewDisplayInterface $display) {
// Act only on comment view displays being disabled.
if ($display->isNew() || $display->getTargetEntityTypeId() !== 'comment' || $display->status()) {
return;
}
$storage = \Drupal::entityTypeManager()->getStorage('entity_view_display');
if (!$storage->loadUnchanged($display->getOriginalId())->status()) {
return;
}
// Disable the comment field formatter when the used view display is disabled.
foreach ($storage->loadMultiple() as $view_display) {
$changed = FALSE;
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $view_display */
foreach ($view_display->getComponents() as $field => $component) {
if (isset($component['type']) && ($component['type'] === 'comment_default')) {
if ($component['settings']['view_mode'] === $display->getMode()) {
$view_display->removeComponent($field);
/** @var \Drupal\Core\Entity\EntityViewModeInterface $mode */
$mode = EntityViewMode::load($display->getTargetEntityTypeId() . '.' . $display->getMode());
$arguments = [
'@id' => $view_display->id(),
'@name' => $field,
'@display' => $mode->label(),
'@mode' => $display->getMode(),
];
\Drupal::logger('system')->warning("View display '@id': Comment field formatter '@name' was disabled because it is using the comment view display '@display' (@mode) that was just disabled.", $arguments);
$changed = TRUE;
}
}
}
if ($changed) {
$view_display->save();
}
}
}
/**
* Implements hook_field_type_category_info_alter().
*/
function comment_field_type_category_info_alter(&$definitions) {
// The `comment` field type belongs in the `general` category, so the
// libraries need to be attached using an alter hook.
$definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'comment/drupal.comment-icon';
}

View File

@@ -0,0 +1,13 @@
administer comments:
title: 'Administer comments and comment settings'
administer comment types:
title: 'Administer comment types and settings'
restrict access: true
access comments:
title: 'View comments'
post comments:
title: 'Post comments'
skip comment approval:
title: 'Skip comment approval'
edit own comments:
title: 'Edit own comments'

View File

@@ -0,0 +1,16 @@
<?php
/**
* @file
* Post update functions for the comment module.
*/
/**
* Implements hook_removed_post_updates().
*/
function comment_removed_post_updates() {
return [
'comment_post_update_enable_comment_admin_view' => '9.0.0',
'comment_post_update_add_ip_address_setting' => '9.0.0',
];
}

View File

@@ -0,0 +1,142 @@
comment.admin:
path: '/admin/content/comment'
defaults:
_title: 'Comments'
_form: '\Drupal\comment\Form\CommentAdminOverview'
type: 'new'
requirements:
_permission: 'administer comments'
comment.admin_approval:
path: '/admin/content/comment/approval'
defaults:
_title: 'Unapproved comments'
_form: '\Drupal\comment\Form\CommentAdminOverview'
type: 'approval'
requirements:
_permission: 'administer comments'
entity.comment.edit_form:
path: '/comment/{comment}/edit'
defaults:
_title: 'Edit'
_entity_form: 'comment.default'
requirements:
_entity_access: 'comment.update'
comment: \d+
comment.approve:
path: '/comment/{comment}/approve'
defaults:
_title: 'Approve'
_controller: '\Drupal\comment\Controller\CommentController::commentApprove'
entity_type: 'comment'
requirements:
_entity_access: 'comment.approve'
_csrf_token: 'TRUE'
comment: \d+
entity.comment.canonical:
path: '/comment/{comment}'
defaults:
_title_callback: '\Drupal\comment\Controller\CommentController::commentPermalinkTitle'
_controller: '\Drupal\comment\Controller\CommentController::commentPermalink'
requirements:
_entity_access: 'comment.view'
comment: \d+
entity.comment.delete_form:
path: '/comment/{comment}/delete'
defaults:
_title: 'Delete'
_entity_form: 'comment.delete'
requirements:
_entity_access: 'comment.delete'
comment: \d+
comment.multiple_delete_confirm:
path: '/admin/content/comment/delete'
defaults:
_title: 'Delete'
_form: '\Drupal\comment\Form\ConfirmDeleteMultiple'
entity_type_id: 'comment'
requirements:
_entity_delete_multiple_access: 'comment'
entity.comment.delete_multiple_form:
path: '/admin/content/comment/delete'
defaults:
_title: 'Delete'
_form: '\Drupal\comment\Form\ConfirmDeleteMultiple'
entity_type_id: 'comment'
requirements:
_entity_delete_multiple_access: 'comment'
comment.reply:
path: '/comment/reply/{entity_type}/{entity}/{field_name}/{pid}'
defaults:
_controller: '\Drupal\comment\Controller\CommentController::getReplyForm'
_title: 'Add new comment'
pid: ~
requirements:
_custom_access: '\Drupal\comment\Controller\CommentController::replyFormAccess'
options:
parameters:
entity:
type: entity:{entity_type}
comment.new_comments_node_links:
path: '/comments/render_new_comments_node_links'
defaults:
_controller: '\Drupal\comment\Controller\CommentController::renderNewCommentsNodeLinks'
requirements:
_permission: 'access content'
comment.node_redirect:
path: '/comment/{node}/reply'
defaults:
_controller: '\Drupal\comment\Controller\CommentController::redirectNode'
requirements:
_entity_access: 'node.view'
_module_dependencies: 'node'
node: \d+
entity.comment_type.collection:
path: '/admin/structure/comment'
defaults:
_entity_list: 'comment_type'
_title: 'Comment types'
requirements:
_permission: 'administer comment types'
options:
_admin_route: TRUE
entity.comment_type.delete_form:
path: '/admin/structure/comment/manage/{comment_type}/delete'
defaults:
_entity_form: 'comment_type.delete'
_title: 'Delete'
requirements:
_entity_access: 'comment_type.delete'
options:
_admin_route: TRUE
entity.comment_type.add_form:
path: '/admin/structure/comment/types/add'
defaults:
_entity_form: 'comment_type.add'
_title: 'Add comment type'
requirements:
_permission: 'administer comment types'
options:
_admin_route: TRUE
entity.comment_type.edit_form:
path: '/admin/structure/comment/manage/{comment_type}'
defaults:
_entity_form: 'comment_type.edit'
_title_callback: '\Drupal\Core\Entity\Controller\EntityController::title'
requirements:
_entity_access: 'comment_type.update'
options:
_admin_route: TRUE

View File

@@ -0,0 +1,29 @@
services:
comment.breadcrumb:
class: Drupal\comment\CommentBreadcrumbBuilder
arguments: ['@entity_type.manager']
tags:
- { name: breadcrumb_builder, priority: 100 }
comment.manager:
class: Drupal\comment\CommentManager
arguments: ['@entity_type.manager', '@config.factory', '@string_translation', '@module_handler', '@current_user', '@entity_field.manager', '@entity_display.repository']
Drupal\comment\CommentManagerInterface: '@comment.manager'
comment.statistics:
autowire: true
class: Drupal\comment\CommentStatistics
arguments: ['@database', '@current_user', '@entity_type.manager', '@state', '@datetime.time', '@database.replica']
tags:
- { name: backend_overridable }
Drupal\comment\CommentStatisticsInterface: '@comment.statistics'
comment.lazy_builders:
class: Drupal\comment\CommentLazyBuilders
arguments: ['@entity_type.manager', '@entity.form_builder', '@current_user', '@comment.manager', '@module_handler', '@renderer']
Drupal\comment\CommentLazyBuilders: '@comment.lazy_builders'
comment.link_builder:
class: Drupal\comment\CommentLinkBuilder
arguments: ['@current_user', '@comment.manager', '@module_handler', '@string_translation', '@entity_type.manager']
Drupal\comment\CommentLinkBuilderInterface: '@comment.link_builder'

View File

@@ -0,0 +1,278 @@
<?php
/**
* @file
* Builds placeholder replacement tokens for comment-related data.
*/
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Render\BubbleableMetadata;
/**
* Implements hook_token_info().
*/
function comment_token_info() {
$type = [
'name' => t('Comments'),
'description' => t('Tokens for comments posted on the site.'),
'needs-data' => 'comment',
];
$tokens = [];
// Provides an integration for each entity type except comment.
foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type_id == 'comment' || !$entity_type->entityClassImplements(ContentEntityInterface::class)) {
continue;
}
if (\Drupal::service('comment.manager')->getFields($entity_type_id)) {
// Get the correct token type.
$token_type = ($entity_type_id == 'taxonomy_term') ? 'term' : $entity_type_id;
// @todo Make this work per field. See https://www.drupal.org/node/2031903.
$tokens[$token_type]['comment-count'] = [
'name' => t("Comment count"),
'description' => t("The number of comments posted on an entity."),
];
$tokens[$token_type]['comment-count-new'] = [
'name' => t("New comment count"),
'description' => t("The number of comments posted on an entity since the reader last viewed it."),
];
}
}
// Core comment tokens
$comment['cid'] = [
'name' => t("Comment ID"),
'description' => t("The unique ID of the comment."),
];
$comment['hostname'] = [
'name' => t("IP Address"),
'description' => t("The IP address of the computer the comment was posted from."),
];
$comment['mail'] = [
'name' => t("Email address"),
'description' => t("The email address left by the comment author."),
];
$comment['homepage'] = [
'name' => t("Home page"),
'description' => t("The home page URL left by the comment author."),
];
$comment['title'] = [
'name' => t("Title"),
'description' => t("The title of the comment."),
];
$comment['body'] = [
'name' => t("Content"),
'description' => t("The formatted content of the comment itself."),
];
$comment['langcode'] = [
'name' => t('Language code'),
'description' => t('The language code of the language the comment is written in.'),
];
$comment['url'] = [
'name' => t("URL"),
'description' => t("The URL of the comment."),
];
$comment['edit-url'] = [
'name' => t("Edit URL"),
'description' => t("The URL of the comment's edit page."),
];
// Chained tokens for comments
$comment['created'] = [
'name' => t("Date created"),
'description' => t("The date the comment was posted."),
'type' => 'date',
];
$comment['changed'] = [
'name' => t("Date changed"),
'description' => t("The date the comment was most recently updated."),
'type' => 'date',
];
$comment['parent'] = [
'name' => t("Parent"),
'description' => t("The comment's parent, if comment threading is active."),
'type' => 'comment',
];
$comment['entity'] = [
'name' => t("Entity"),
'description' => t("The entity the comment was posted to."),
'type' => 'entity',
];
$comment['author'] = [
'name' => t("Author"),
'description' => t("The author name of the comment."),
'type' => 'user',
];
return [
'types' => ['comment' => $type],
'tokens' => [
'comment' => $comment,
] + $tokens,
];
}
/**
* Implements hook_tokens().
*/
function comment_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
$token_service = \Drupal::token();
$url_options = ['absolute' => TRUE];
if (isset($options['langcode'])) {
$url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
$langcode = $options['langcode'];
}
else {
$langcode = NULL;
}
$replacements = [];
if ($type == 'comment' && !empty($data['comment'])) {
/** @var \Drupal\comment\CommentInterface $comment */
$comment = $data['comment'];
foreach ($tokens as $name => $original) {
switch ($name) {
// Simple key values on the comment.
case 'cid':
$replacements[$original] = $comment->id();
break;
// Poster identity information for comments.
case 'hostname':
$replacements[$original] = $comment->getHostname();
break;
case 'mail':
$mail = $comment->getAuthorEmail();
// Add the user cacheability metadata in case the author of the comment
// is not the anonymous user.
if ($comment->getOwnerId()) {
$bubbleable_metadata->addCacheableDependency($comment->getOwner());
}
$replacements[$original] = $mail;
break;
case 'homepage':
$replacements[$original] = UrlHelper::stripDangerousProtocols($comment->getHomepage());
break;
case 'title':
$replacements[$original] = $comment->getSubject();
break;
case 'body':
// "processed" returns a \Drupal\Component\Render\MarkupInterface via
// check_markup().
$replacements[$original] = $comment->comment_body->processed;
break;
case 'langcode':
$replacements[$original] = $comment->language()->getId();
break;
// Comment related URLs.
case 'url':
$url_options['fragment'] = 'comment-' . $comment->id();
$replacements[$original] = $comment->toUrl('canonical', $url_options)->toString();
break;
case 'edit-url':
$url_options['fragment'] = NULL;
$replacements[$original] = $comment->toUrl('edit-form', $url_options)->toString();
break;
case 'author':
$name = $comment->getAuthorName();
// Add the user cacheability metadata in case the author of the comment
// is not the anonymous user.
if ($comment->getOwnerId()) {
$bubbleable_metadata->addCacheableDependency($comment->getOwner());
}
$replacements[$original] = $name;
break;
case 'parent':
if ($comment->hasParentComment()) {
$parent = $comment->getParentComment();
$bubbleable_metadata->addCacheableDependency($parent);
$replacements[$original] = $parent->getSubject();
}
break;
case 'created':
$date_format = DateFormat::load('medium');
$bubbleable_metadata->addCacheableDependency($date_format);
$replacements[$original] = \Drupal::service('date.formatter')->format($comment->getCreatedTime(), 'medium', '', NULL, $langcode);
break;
case 'changed':
$date_format = DateFormat::load('medium');
$bubbleable_metadata->addCacheableDependency($date_format);
$replacements[$original] = \Drupal::service('date.formatter')->format($comment->getChangedTime(), 'medium', '', NULL, $langcode);
break;
case 'entity':
$entity = $comment->getCommentedEntity();
$bubbleable_metadata->addCacheableDependency($entity);
$title = $entity->label();
$replacements[$original] = $title;
break;
}
}
// Chained token relationships.
if ($entity_tokens = $token_service->findwithPrefix($tokens, 'entity')) {
$entity = $comment->getCommentedEntity();
$replacements += $token_service->generate($comment->getCommentedEntityTypeId(), $entity_tokens, [$comment->getCommentedEntityTypeId() => $entity], $options, $bubbleable_metadata);
}
if ($date_tokens = $token_service->findwithPrefix($tokens, 'created')) {
$replacements += $token_service->generate('date', $date_tokens, ['date' => $comment->getCreatedTime()], $options, $bubbleable_metadata);
}
if ($date_tokens = $token_service->findwithPrefix($tokens, 'changed')) {
$replacements += $token_service->generate('date', $date_tokens, ['date' => $comment->getChangedTime()], $options, $bubbleable_metadata);
}
if (($parent_tokens = $token_service->findwithPrefix($tokens, 'parent')) && $parent = $comment->getParentComment()) {
$replacements += $token_service->generate('comment', $parent_tokens, ['comment' => $parent], $options, $bubbleable_metadata);
}
if (($author_tokens = $token_service->findwithPrefix($tokens, 'author')) && $account = $comment->getOwner()) {
$replacements += $token_service->generate('user', $author_tokens, ['user' => $account], $options, $bubbleable_metadata);
}
}
// Replacement tokens for any content entities that have comment field.
elseif (!empty($data[$type]) && $data[$type] instanceof FieldableEntityInterface) {
/** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
$entity = $data[$type];
foreach ($tokens as $name => $original) {
switch ($name) {
case 'comment-count':
$count = 0;
$fields = array_keys(\Drupal::service('comment.manager')->getFields($entity->getEntityTypeId()));
$definitions = array_keys($entity->getFieldDefinitions());
$valid_fields = array_intersect($fields, $definitions);
foreach ($valid_fields as $field_name) {
$count += $entity->get($field_name)->comment_count;
}
$replacements[$original] = $count;
break;
case 'comment-count-new':
$replacements[$original] = \Drupal::service('comment.manager')->getCountNewComments($entity);
break;
}
}
}
return $replacements;
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* @file
* Provide views data for comment.module.
*/
use Drupal\Core\Entity\ContentEntityInterface;
/**
* Implements hook_views_data_alter().
*/
function comment_views_data_alter(&$data) {
// New comments are only supported for node table because it requires the
// history table.
$data['node']['new_comments'] = [
'title' => t('New comments'),
'help' => t('The number of new comments on the node.'),
'field' => [
'id' => 'node_new_comments',
'no group by' => TRUE,
],
];
// Provides an integration for each entity type except comment.
foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type_id == 'comment' || !$entity_type->entityClassImplements(ContentEntityInterface::class) || !$entity_type->getBaseTable()) {
continue;
}
$fields = \Drupal::service('comment.manager')->getFields($entity_type_id);
$base_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable();
$args = ['@entity_type' => $entity_type_id];
if ($fields) {
$data[$base_table]['comments_link'] = [
'field' => [
'title' => t('Add comment link'),
'help' => t('Display the standard add comment link used on regular @entity_type, which will only display if the viewing user has access to add a comment.', $args),
'id' => 'comment_entity_link',
],
];
// Multilingual properties are stored in data table.
if (!($table = $entity_type->getDataTable())) {
$table = $entity_type->getBaseTable();
}
$data[$table]['uid_touch'] = [
'title' => t('User posted or commented'),
'help' => t('Display nodes only if a user posted the @entity_type or commented on the @entity_type.', $args),
'argument' => [
'field' => 'uid',
'name table' => 'users_field_data',
'name field' => 'name',
'id' => 'argument_comment_user_uid',
'no group by' => TRUE,
'entity_type' => $entity_type_id,
'entity_id' => $entity_type->getKey('id'),
],
'filter' => [
'field' => 'uid',
'name table' => 'users_field_data',
'name field' => 'name',
'id' => 'comment_user_uid',
'entity_type' => $entity_type_id,
'entity_id' => $entity_type->getKey('id'),
],
];
foreach ($fields as $field_name => $field) {
$data[$base_table][$field_name . '_cid'] = [
'title' => t('Comments of the @entity_type using field: @field_name', $args + ['@field_name' => $field_name]),
'help' => t('Relate all comments on the @entity_type. This will create 1 duplicate record for every comment. Usually if you need this it is better to create a comment view.', $args),
'relationship' => [
'group' => t('Comment'),
'label' => t('Comments'),
'base' => 'comment_field_data',
'base field' => 'entity_id',
'relationship field' => $entity_type->getKey('id'),
'id' => 'standard',
'extra' => [
[
'field' => 'entity_type',
'value' => $entity_type_id,
],
[
'field' => 'field_name',
'value' => $field_name,
],
],
],
];
}
}
}
}

View File

@@ -0,0 +1,10 @@
/*
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/3084859
* @preserve
*/
.field-icon-comment {
color: red;
background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m21.0909 34-4.3273-5.4634h-8.03633c-.40988 0-.80297-.1645-1.0928-.4572-.28983-.2928-.45265-.6898-.45265-1.1038v-17.00994c0-.414.16282-.81104.45265-1.10378s.68292-.4572 1.0928-.4572h24.72723c.4099 0 .803.16446 1.0928.4572.2899.29274.4527.68978.4527 1.10378v17.00994c0 .414-.1628.811-.4527 1.1038-.2898.2927-.6829.4572-1.0928.4572h-8.0363zm2.8421-8.5854h7.9761v-13.888h-21.6364v13.888h7.9761l2.8421 3.5872zm-21.38755-23.4146h26.27275v3.12195h-24.72729v17.17075h-3.09091v-18.73172c0-.414.16282-.81104.45265-1.10378s.68292-.4572 1.0928-.4572z' fill='%2355565b'/%3e%3c/svg%3e");
}

View File

@@ -0,0 +1,4 @@
.field-icon-comment {
color: red;
background-image: url(../../../misc/icons/55565b/comment.svg);
}

View File

@@ -0,0 +1,43 @@
---
label: 'Configuring comments'
related:
- comment.overview
- comment.creating_type
- comment.disabling
- field_ui.add_field
---
{% set comment_permissions_link_text %}
{% trans %}Administer comments and comment settings{% endtrans %}
{% endset %}
{% set comment_permissions_link = render_var(help_route_link(comment_permissions_link_text, 'user.admin_permissions.module', {'modules': 'comment'})) %}
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
{% set comment_type_topic = render_var(help_topic_link('comment.creating_type')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure a content entity type/subtype to allow commenting, using a comment type that you have configured. See {{ content_structure_topic }} for more about content entities and fields, and {{ comment_type_topic }} to configure a comment type.{% endtrans %}</p>
<h2>{% trans %}Who can configure comments?{% endtrans %}</h2>
<p>{% trans %}In order to follow these steps, the Field UI module must be installed. You'll need the Comment module's <em>{{ comment_permissions_link }}</em> permission, in order to change comment settings for a comment field. You'll also need to have the appropriate permission for adding fields to the entity type or subtype that the comments are attached to. For example, to add a comment field to content items provided by the Node module, you would need the Node module's <em>Administer content types</em> permission.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Follow the steps in the related <em>Adding a field to an entity sub-type</em> topic to add a field of type <em>Comments</em> to the desired entity type or sub-type.{% endtrans %}</li>
<li>{% trans %}On the first field settings page, choose the <em>Comment type</em> to use for this entity type or sub-type. You'll also notice that the <em>Allowed number of values</em> field cannot be changed for comment fields.{% endtrans %}</li>
<li>{% trans %}On the next field settings page, enter the desired settings for the comment field:{% endtrans %}
<ul>
<li>{% trans %}<em>Threading</em>: whether or not the comments are collected by threads, with people able to reply to particular comments instead of to the content entity itself.{% endtrans %}</li>
<li>{% trans %}<em>Comments per page</em>: the maximum number of comments displayed on one page (a pager will be added if you exceed this limit).{% endtrans %}</li>
<li>{% trans %}<em>Anonymous commenting</em>: whether or not anonymous commenters are allowed or required to leave contact information with their comments (only applies if anonymous users have permission to post comments).{% endtrans %}</li>
<li>{% trans %}<em>Show reply form on the same page as comments</em>: whether the comment reply form is displayed on the same page as the comments. If this is not selected, clicking <em>Reply</em> will open a new page with the reply form.{% endtrans %}</li>
<li>{% trans %}<em>Preview comments</em>: whether previewing comments before submission is <em>Required</em>, <em>Optional</em>, or <em>Disabled</em>.{% endtrans %}</li>
<li>{% trans %}<em>Default value</em>: each individual entity has its own comment settings, but here you can set defaults for the comment settings for this entity type or subtype. The comment settings values are:{% endtrans %}
<ul>
<li>{% trans %}<em>Open</em>: comments are allowed.{% endtrans %}</li>
<li>{% trans %}<em>Closed</em>: past comments remain visible, but no new comments are allowed.{% endtrans %}</li>
<li>{% trans %}<em>Hidden</em>: past comments are hidden, and no new comments are allowed.{% endtrans %}</li>
</ul>
</li>
</ul>
</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/docs/8/core/modules/comment/administering-a-content-types-comment-settings">{% trans %}Online documentation for content comment settings{% endtrans %}</a></li>
</ul>

View File

@@ -0,0 +1,32 @@
---
label: 'Creating a comment type'
related:
- field_ui.add_field
- field_ui.manage_display
- field_ui.manage_form
- comment.overview
- comment.configuring
---
{% set comment_types_link_text %}
{% trans %}Comment types{% endtrans %}
{% endset %}
{% set comment_types_link = render_var(help_route_link(comment_types_link_text, 'entity.comment_type.collection')) %}
{% set comment_permissions_link_text %}
{% trans %}Administer comment types and settings{% endtrans %}
{% endset %}
{% set comment_permissions_link = render_var(help_route_link(comment_permissions_link_text, 'user.admin_permissions.module', {'modules': 'comment'})) %}
{% set comment_overview_topic = render_var(help_topic_link('comment.overview')) %}
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Create a new comment type. See {{ comment_overview_topic }} for information about comments and comment types.{% endtrans %}</p>
<h2>{% trans %}Who can create a comment type?{% endtrans %}</h2>
<p>{% trans %}Users with the <em>{{ comment_permissions_link }}</em> permission (typically administrators) can create comment types.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>{{ comment_types_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add comment type</em>.{% endtrans %}</li>
<li>{% trans %}In the <em>Label</em> field, enter a name for the comment type, which is how it will be listed in the administrative interface.{% endtrans %}</li>
<li>{% trans %}In the <em>Target entity type</em> field, select the entity type to be commented on. See {{ content_structure_topic }} for more about content entities and fields.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>. The comment type will be created.{% endtrans %}</li>
<li>{% trans %}Optionally, if you have the core Field UI module installed you can follow the steps in the related topics to add fields to the new comment type, set up the editing form, and configure the display.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,28 @@
---
label: 'Disabling comments'
related:
- comment.overview
- comment.configuring
---
{% set comment_permissions_text %}
{% trans %}Administer comments and comment settings{% endtrans %}
{% endset %}
{% set comment_permissions_link = render_var(help_route_link(comment_permissions_text, 'user.admin_permissions.module', {'modules': 'comment'})) %}
{% set comment_config_topic = render_var(help_topic_link('comment.configuring')) %}
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Turn off commenting for a particular entity (see {{ content_structure_topic }} for more about content entities and fields). Note that if you want to turn off commenting for all entities of an entity type or subtype, you will need to edit the field settings for the comment field; see {{ comment_config_topic }} for more about configuring the comment field.{% endtrans %}</p>
<h2>{% trans %}Who can disable comments?{% endtrans %}</h2>
<p>{% trans %}You will need the <em>{{ comment_permissions_link }}</em> permission in order to disable commenting. You will also need permission to edit the entity that the comments are on.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Find the entity you want to disable comments for, and edit it. For example, to turn off comments on a content item, you could find it by navigating in the <em>Manage</em> administrative menu to <em>Content</em>, filtering to find the content item, and clicking <em>Edit</em>.{% endtrans %}</li>
<li>{% trans %}Under <em>Comment settings</em>, select the desired comment setting:{% endtrans %}
<ul>
<li>{% trans %}<em>Open</em>: comments are allowed.{% endtrans %}</li>
<li>{% trans %}<em>Closed</em>: past comments remain visible, but no new comments are allowed.{% endtrans %}</li>
<li>{% trans %}<em>Hidden</em>: past comments are hidden, and no new comments are allowed.{% endtrans %}</li>
</ul>
</li>
<li>{% trans %}Save the entity.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,34 @@
---
label: 'Moderating comments'
related:
- comment.overview
- comment.configuring
- comment.disabling
---
{% set comment_unpublished_link_text %}
{% trans %}Unapproved comments{% endtrans %}
{% endset %}
{% set comment_unpublished_link = render_var(help_route_link(comment_unpublished_link_text, 'comment.admin_approval')) %}
{% set comment_published_link_text %}
{% trans %}Comments{% endtrans %}
{% endset %}
{% set comment_published_link = render_var(help_route_link(comment_published_link_text, 'comment.admin')) %}
{% set comment_permissions_link_text %}
{% trans %}Administer comments and comment settings{% endtrans %}
{% endset %}
{% set comment_permissions_link = render_var(help_route_link(comment_permissions_link_text, 'user.admin_permissions.module', {'modules': 'comment'})) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Decide which comments are shown on the website.{% endtrans %}</p>
<h2>{% trans %}Who can moderate comments?{% endtrans %}</h2>
<p>{% trans %}Users with the <em>{{ comment_permissions_link }}</em> permission (typically administrators) can moderate comments. You will also need the <em>Access the Content Overview page</em> permission from the Node module (if it is installed) to navigate to the comment management page.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Content</em> &gt; <em>{{ comment_published_link }}</em>. A list of all comments is shown.{% endtrans %}</li>
<li>{% trans %}To unpublish comments, select one or more comments by checking the boxes on the left side (right side in right-to-left languages). Then select <em>Unpublish comment</em> from the <em>Action</em> select list and click <em>Apply to selected items</em>. If you select the <em>Delete comment</em> action, you can instead delete the unwanted comments.{% endtrans %}</li>
<li>{% trans %}To change the content of a comment click <em>Edit</em> from the dropdown button for a particular comment.{% endtrans %}</li>
<li>{% trans %}To publish comments that are not yet visible on the website, navigate to the <em>{{ comment_unpublished_link }}</em> tab. Select one or more comments by checking the boxes on the left side (right side in right-to-left languages). Then select <em>Publish comment</em> from the <em>Action</em> select list and click <em>Apply to selected items</em>.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/docs/8/core/modules/comment/administering-and-approving-comments">{% trans %}Online documentation for moderating comments{% endtrans %}</a></li>
</ul>

View File

@@ -0,0 +1,29 @@
---
label: 'Managing comments'
top_level: true
related:
- comment.moderating
- comment.configuring
- comment.creating_type
- comment.disabling
- field_ui.add_field
- field_ui.manage_display
- field_ui.manage_form
---
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
{% set users_overview_topic = render_var(help_topic_link('user.overview')) %}
<h2>{% trans %}What is a comment?{% endtrans %}</h2>
<p>{% trans %}A comment is a piece of content, typically posted by a website visitor, which provides discussion or commentary on other content like blog posts and news articles. Comments are a type of content entity, and can have fields that store text, HTML markup, and other data. Comments are attached to other content entities via Comment fields. See {{ content_structure_topic }} for more about content entities and fields.{% endtrans %}</p>
<h2>{% trans %}What is a comment type?{% endtrans %}</h2>
<p>{% trans %}Comments are divided into <em>comment types</em>, which are the entity sub-types for the comment entity type. Each comment type has its own fields and its own form and display settings; each type can be used to comment on a single entity type. You can set up different comment types for different commenting purposes on your web site; for example, you might set up a comment type for recipes that has fields "How did it taste?" and "Did the instructions work?", and another comment type for blog entries that has only a generic comment body field.{% endtrans %}</p>
<h2>{% trans %}What is moderation?{% endtrans %}</h2>
<p>{% trans %}<em>Moderation</em> is a workflow where comments posted by some users on your site are verified before being published, to prevent spam and other bad behavior. The core software provides basic moderation functionality: you can configure permissions so that new comments posted by some user roles start as unpublished until a user with a different role reviews and publishes them. Contributed modules provide additional moderation and spam-reduction functionality, such as requiring untrusted users pass a CAPTCHA test before submitting comments and letting community members flag comments as possible spam. See {{ users_overview_topic }} for more about users, permissions, and roles.{% endtrans %}</p>
<h2>{% trans %}Overview of managing comments{% endtrans %}</h2>
<p>{% trans %}The core Comment module provides the following functionality:{% endtrans %}</p>
<ul>
<li>{% trans %}Posting comments{% endtrans %}</li>
<li>{% trans %}Creating comment types; the core Field UI module allows you to attach fields to comment types and attach comment reference fields to other entities so that people can comment on them.{% endtrans %}</li>
<li>{% trans %}Configuring commenting{% endtrans %}</li>
<li>{% trans %}Moderating comments as discussed above{% endtrans %}</li>
</ul>
<p>{% trans %}See the related topics listed below for specific tasks.{% endtrans %}</p>

View File

@@ -0,0 +1,25 @@
/**
* @file
* Attaches behaviors for the Comment module's "by-viewer" class.
*/
(function ($, Drupal, drupalSettings) {
/**
* Add 'by-viewer' class to comments written by the current user.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.commentByViewer = {
attach(context) {
const currentUserID = parseInt(drupalSettings.user.uid, 10);
$('[data-comment-user-id]')
.filter(function () {
return (
parseInt(this.getAttribute('data-comment-user-id'), 10) ===
currentUserID
);
})
.addClass('by-viewer');
},
};
})(jQuery, Drupal, drupalSettings);

View File

@@ -0,0 +1,103 @@
/**
* @file
* Attaches behaviors for the Comment module's "new" indicator.
*
* May only be loaded for authenticated users, with the History module
* installed.
*/
(function ($, Drupal, window) {
/**
* Processes the markup for "new comment" indicators.
*
* @param {Array.<Element>} placeholders
* The elements that should be processed.
*/
function processCommentNewIndicators(placeholders) {
let isFirstNewComment = true;
const newCommentString = Drupal.t('new');
let $placeholder;
placeholders.forEach((placeholder) => {
$placeholder = $(placeholder);
const timestamp = parseInt(
$placeholder.attr('data-comment-timestamp'),
10,
);
const $node = $placeholder.closest('[data-history-node-id]');
const nodeID = $node.attr('data-history-node-id');
const lastViewTimestamp = Drupal.history.getLastRead(nodeID);
if (timestamp > lastViewTimestamp) {
// Turn the placeholder into an actual "new" indicator.
placeholder.textContent = newCommentString;
$placeholder
.removeClass('hidden')
.closest('.js-comment')
// Add 'new' class to the comment, so it can be styled.
.addClass('new');
// Insert "new" anchor just before the "comment-<cid>" anchor if
// this is the first new comment in the DOM.
if (isFirstNewComment) {
isFirstNewComment = false;
$placeholder.prev().before('<a id="new"></a>');
// If the URL points to the first new comment, then scroll to that
// comment.
if (window.location.hash === '#new') {
window.scrollTo(
0,
$placeholder.offset().top - Drupal.displace.offsets.top,
);
}
}
}
});
}
/**
* Renders "new" comment indicators wherever necessary.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches "new" comment indicators behavior.
*/
Drupal.behaviors.commentNewIndicator = {
attach(context) {
// Collect all "new" comment indicator placeholders (and their
// corresponding node IDs) newer than 30 days ago that have not already
// been read after their last comment timestamp.
const nodeIDs = [];
const placeholders = once(
'history',
'[data-comment-timestamp]',
context,
).filter((placeholder) => {
const $placeholder = $(placeholder);
const commentTimestamp = parseInt(
$placeholder.attr('data-comment-timestamp'),
10,
);
const nodeID = $placeholder
.closest('[data-history-node-id]')
.attr('data-history-node-id');
if (Drupal.history.needsServerCheck(nodeID, commentTimestamp)) {
nodeIDs.push(nodeID);
return true;
}
return false;
});
if (placeholders.length === 0) {
return;
}
// Fetch the node read timestamps from the server.
Drupal.history.fetchTimestamps(nodeIDs, () => {
processCommentNewIndicators(placeholders);
});
},
};
})(jQuery, Drupal, window);

View File

@@ -0,0 +1,195 @@
/**
* @file
* Attaches behaviors for the Comment module's "X new comments" link.
*
* May only be loaded for authenticated users, with the History module
* installed.
*/
(function ($, Drupal, drupalSettings) {
/**
* Hides a "new comment" element.
*
* @param {jQuery} $placeholder
* The placeholder element of the new comment link.
*
* @return {jQuery}
* The placeholder element passed in as a parameter.
*/
function hide($placeholder) {
return (
$placeholder
// Find the parent <li>.
.closest('.comment-new-comments')
// Find the preceding <li>, if any, and give it the 'last' class.
.prev()
.addClass('last')
// Go back to the parent <li> and hide it.
.end()
.hide()
);
}
/**
* Removes a "new comment" element.
*
* @param {jQuery} $placeholder
* The placeholder element of the new comment link.
*/
function remove($placeholder) {
hide($placeholder).remove();
}
/**
* Shows a "new comment" element.
*
* @param {jQuery} $placeholder
* The placeholder element of the new comment link.
*
* @return {jQuery}
* The placeholder element passed in as a parameter.
*/
function show($placeholder) {
return (
$placeholder
// Find the parent <li>.
.closest('.comment-new-comments')
// Find the preceding <li>, if any, and remove its 'last' class, if any.
.prev()
.removeClass('last')
// Go back to the parent <li> and show it.
.end()
.show()
);
}
/**
* Processes new comment links and adds appropriate text in relevant cases.
*
* @param {Array.<Element>} placeholders
* The placeholder elements of the current page.
*/
function processNodeNewCommentLinks(placeholders) {
// Figure out which placeholders need the "x new comments" links.
const $placeholdersToUpdate = {};
let fieldName = 'comment';
let $placeholder;
placeholders.forEach((placeholder) => {
$placeholder = $(placeholder);
const timestamp = parseInt(
$placeholder.attr('data-history-node-last-comment-timestamp'),
10,
);
fieldName = $placeholder.attr('data-history-node-field-name');
const nodeID = $placeholder
.closest('[data-history-node-id]')
.attr('data-history-node-id');
const lastViewTimestamp = Drupal.history.getLastRead(nodeID);
// Queue this placeholder's "X new comments" link to be downloaded from
// the server.
if (timestamp > lastViewTimestamp) {
$placeholdersToUpdate[nodeID] = $placeholder;
}
// No "X new comments" link necessary; remove it from the DOM.
else {
remove($placeholder);
}
});
// Perform an AJAX request to retrieve node view timestamps.
const nodeIDs = Object.keys($placeholdersToUpdate);
if (nodeIDs.length === 0) {
return;
}
/**
* Renders the "X new comments" links.
*
* Either use the data embedded in the page or perform an AJAX request to
* retrieve the same data.
*
* @param {object} results
* Data about new comment links indexed by nodeID.
*/
function render(results) {
Object.keys(results || {}).forEach((nodeID) => {
if ($placeholdersToUpdate.hasOwnProperty(nodeID)) {
const $placeholderItem = $placeholdersToUpdate[nodeID];
const result = results[nodeID];
$placeholderItem[0].textContent = Drupal.formatPlural(
result.new_comment_count,
'1 new comment',
'@count new comments',
);
$placeholderItem
.attr('href', result.first_new_comment_link)
.removeClass('hidden');
show($placeholderItem);
}
});
}
if (drupalSettings.comment && drupalSettings.comment.newCommentsLinks) {
render(drupalSettings.comment.newCommentsLinks.node[fieldName]);
} else {
$.ajax({
url: Drupal.url('comments/render_new_comments_node_links'),
type: 'POST',
data: { 'node_ids[]': nodeIDs, field_name: fieldName },
dataType: 'json',
success: render,
});
}
}
/**
* Render "X new comments" links wherever necessary.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches new comment links behavior.
*/
Drupal.behaviors.nodeNewCommentsLink = {
attach(context) {
// Collect all "X new comments" node link placeholders (and their
// corresponding node IDs) newer than 30 days ago that have not already
// been read after their last comment timestamp.
const nodeIDs = [];
const placeholders = once(
'history',
'[data-history-node-last-comment-timestamp]',
context,
).filter((placeholder) => {
const $placeholder = $(placeholder);
const lastCommentTimestamp = parseInt(
$placeholder.attr('data-history-node-last-comment-timestamp'),
10,
);
const nodeID = $placeholder
.closest('[data-history-node-id]')
.attr('data-history-node-id');
if (Drupal.history.needsServerCheck(nodeID, lastCommentTimestamp)) {
nodeIDs.push(nodeID);
// Hide this placeholder link until it is certain we'll need it.
hide($placeholder);
return true;
}
// Remove this placeholder link from the DOM because we won't need it.
remove($placeholder);
return false;
});
if (placeholders.length === 0) {
return;
}
// Perform an AJAX request to retrieve node read timestamps.
Drupal.history.fetchTimestamps(nodeIDs, () => {
processNodeNewCommentLinks(placeholders);
});
},
};
})(jQuery, Drupal, drupalSettings);

View File

@@ -0,0 +1,73 @@
id: d6_comment
label: Comments
audit: true
migration_tags:
- Drupal 6
- Content
source:
plugin: d6_comment
constants:
entity_type: node
process:
# If you are using this file to build a custom migration consider removing
# the cid field to allow incremental migrations.
cid: cid
pid:
-
plugin: skip_on_empty
method: process
source: pid
-
plugin: migration_lookup
migration: d6_comment
entity_id:
-
plugin: migration_lookup
migration:
- d6_node_complete
- d6_node
- d6_node_translation
source: nid
-
plugin: node_complete_node_lookup
-
plugin: skip_on_empty
method: row
entity_type: 'constants/entity_type'
comment_type:
-
plugin: migration_lookup
source: type
migration: d6_comment_type
-
plugin: skip_on_empty
method: row
langcode: language
field_name: '@comment_type'
subject: subject
uid: uid
name: name
mail: mail
homepage: homepage
hostname: hostname
created: timestamp
changed: timestamp
status: status #In D6, published=0. We reverse the value in prepareRow.
thread: thread
'comment_body/value': comment
'comment_body/format':
plugin: migration_lookup
migration: d6_filter_format
source: format
destination:
plugin: entity:comment
migration_dependencies:
required:
- d6_node
- d6_comment_type
- d6_comment_entity_display
- d6_comment_entity_form_display
- d6_user
- d6_filter_format
optional:
- d6_node_translation

View File

@@ -0,0 +1,32 @@
id: d6_comment_entity_display
label: Comment display configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: comment_type
constants:
entity_type: node
view_mode: default
options:
label: hidden
type: comment_default
weight: 20
process:
entity_type: 'constants/entity_type'
field_name:
-
plugin: migration_lookup
source: type
migration: d6_comment_type
-
plugin: skip_on_empty
method: row
view_mode: 'constants/view_mode'
options: 'constants/options'
bundle: type
destination:
plugin: component_entity_display
migration_dependencies:
required:
- d6_comment_field_instance

View File

@@ -0,0 +1,31 @@
id: d6_comment_entity_form_display
label: Comment field form display configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: comment_type
constants:
entity_type: node
form_mode: default
options:
type: comment_default
weight: 20
process:
entity_type: 'constants/entity_type'
field_name:
-
plugin: migration_lookup
source: type
migration: d6_comment_type
-
plugin: skip_on_empty
method: row
form_mode: 'constants/form_mode'
options: 'constants/options'
bundle: type
destination:
plugin: component_entity_form_display
migration_dependencies:
required:
- d6_comment_field_instance

View File

@@ -0,0 +1,41 @@
id: d6_comment_entity_form_display_subject
label: Comment subject form display configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: comment_type
constants:
entity_type: comment
field_name: subject
form_mode: default
options:
type: string_textfield
weight: 10
process:
entity_type: 'constants/entity_type'
field_name: 'constants/field_name'
form_mode: 'constants/form_mode'
options: 'constants/options'
bundle:
-
plugin: migration_lookup
source: type
migration: d6_comment_type
-
plugin: skip_on_empty
method: row
hidden:
plugin: static_map
source: comment_subject_field
map:
# If comment_subject_field = FALSE, then hidden = TRUE.
0: true
# If comment_subject_field = TRUE, then hidden = FALSE.
1: false
default_value: false
destination:
plugin: component_entity_form_display
migration_dependencies:
required:
- d6_comment_type

View File

@@ -0,0 +1,27 @@
id: d6_comment_field
label: Comment field configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: comment_type
constants:
entity_type: node
type: comment
process:
entity_type: 'constants/entity_type'
field_name:
-
plugin: migration_lookup
source: type
migration: d6_comment_type
-
plugin: skip_on_empty
method: row
type: 'constants/type'
'settings/comment_type': '@field_name'
destination:
plugin: entity:field_storage_config
migration_dependencies:
required:
- d6_comment_type

View File

@@ -0,0 +1,74 @@
id: d6_comment_field_instance
label: Comment field instance configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: comment_type
constants:
entity_type: node
label: Comments
required: true
process:
entity_type: 'constants/entity_type'
label: 'constants/label'
required: 'constants/required'
field_name:
-
plugin: migration_lookup
source: type
migration: d6_comment_type
-
plugin: skip_on_empty
method: row
bundle: type
'default_value/0/status':
# We're using static_map instead of default_value otherwise if the source
# is 0, the default value of 1 would be used.
plugin: static_map
source: comment
map:
0: 0
1: 1
2: 2
default_value: 2
'settings/default_mode':
plugin: static_map
source: comment_default_mode
map:
# COMMENT_MODE_FLAT_COLLAPSED --> COMMENT_MODE_FLAT
1: 0
# COMMENT_MODE_FLAT_EXPANDED --> COMMENT_MODE_FLAT
2: 0
# COMMENT_MODE_THREADED_COLLAPSED --> COMMENT_MODE_THREADED
3: 1
# COMMENT_MODE_THREADED_EXPANDED --> COMMENT_MODE_THREADED
4: 1
default_value: 1
'settings/per_page':
plugin: default_value
source: comment_default_per_page
default_value: 50
'settings/anonymous':
plugin: default_value
source: comment_anonymous
default_value: 0
'settings/form_location':
plugin: default_value
source: comment_form_location
default_value: 0
'settings/preview':
# We're using static_map instead of default_value otherwise if the source
# is 0, the default value of 1 would be used.
plugin: static_map
source: comment_preview
map:
0: 0
1: 1
default_value: 1
destination:
plugin: entity:field_config
migration_dependencies:
required:
- d6_node_type
- d6_comment_field

View File

@@ -0,0 +1,40 @@
id: d6_comment_type
label: Comment type
migration_tags:
- Drupal 6
- Configuration
source:
plugin: comment_type
constants:
entity_type: node
id_prefix: 'comment_node_'
label_suffix: 'comment'
process:
target_entity_type_id: 'constants/entity_type'
id:
-
plugin: concat
source:
- 'constants/id_prefix'
- type
-
plugin: static_map
bypass: true
# The Forum module provides its own comment type (comment_forum), which we
# want to reuse if it exists.
map:
comment_node_forum: comment_forum
-
plugin: make_unique_entity_field
entity_type: comment_type
field: id
length: 30
migrated: true
label:
plugin: concat
source:
- name
- 'constants/label_suffix'
delimiter: ' '
destination:
plugin: entity:comment_type

View File

@@ -0,0 +1,70 @@
id: d7_comment
label: Comments
audit: true
migration_tags:
- Drupal 7
- Content
class: Drupal\comment\Plugin\migrate\D7Comment
source:
plugin: d7_comment
constants:
entity_type: node
process:
# If you are using this file to build a custom migration consider removing
# the cid field to allow incremental migrations.
cid: cid
pid:
-
plugin: skip_on_empty
method: process
source: pid
-
plugin: migration_lookup
migration: d7_comment
entity_id:
-
plugin: migration_lookup
migration:
- d7_node_complete
- d7_node
- d7_node_translation
source: nid
-
plugin: node_complete_node_lookup
-
plugin: skip_on_empty
method: row
entity_type: 'constants/entity_type'
comment_type:
-
plugin: migration_lookup
source: node_type
migration: d7_comment_type
-
plugin: skip_on_empty
method: row
# Comments migrated from Drupal 6 to Drupal 7 may not have a language.
langcode:
plugin: default_value
source: language
default_value: und
field_name: '@comment_type'
subject: subject
uid: uid
name: name
mail: mail
homepage: homepage
hostname: hostname
created: created
changed: changed
status: status
thread: thread
comment_body: comment_body
destination:
plugin: entity:comment
migration_dependencies:
required:
- d7_node
- d7_comment_type
optional:
- d7_node_translation

View File

@@ -0,0 +1,32 @@
id: d7_comment_entity_display
label: Comment display configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: comment_type
constants:
entity_type: node
view_mode: default
options:
label: hidden
type: comment_default
weight: 20
process:
entity_type: 'constants/entity_type'
field_name:
-
plugin: migration_lookup
source: type
migration: d7_comment_type
-
plugin: skip_on_empty
method: row
view_mode: 'constants/view_mode'
options: 'constants/options'
bundle: type
destination:
plugin: component_entity_display
migration_dependencies:
required:
- d7_comment_field_instance

View File

@@ -0,0 +1,31 @@
id: d7_comment_entity_form_display
label: Comment field form display configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: comment_type
constants:
entity_type: node
form_mode: default
options:
type: comment_default
weight: 20
process:
entity_type: 'constants/entity_type'
field_name:
-
plugin: migration_lookup
source: type
migration: d7_comment_type
-
plugin: skip_on_empty
method: row
form_mode: 'constants/form_mode'
options: 'constants/options'
bundle: type
destination:
plugin: component_entity_form_display
migration_dependencies:
required:
- d7_comment_field_instance

View File

@@ -0,0 +1,41 @@
id: d7_comment_entity_form_display_subject
label: Comment subject form display configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: comment_type
constants:
entity_type: comment
field_name: subject
form_mode: default
options:
type: string_textfield
weight: 10
process:
entity_type: 'constants/entity_type'
field_name: 'constants/field_name'
form_mode: 'constants/form_mode'
options: 'constants/options'
bundle:
-
plugin: migration_lookup
source: type
migration: d7_comment_type
-
plugin: skip_on_empty
method: row
hidden:
plugin: static_map
source: comment_subject_field
map:
# If comment_subject_field = FALSE, then hidden = TRUE.
0: true
# If comment_subject_field = TRUE, then hidden = FALSE.
1: false
default_value: false
destination:
plugin: component_entity_form_display
migration_dependencies:
required:
- d7_comment_type

View File

@@ -0,0 +1,27 @@
id: d7_comment_field
label: Comment field configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: comment_type
constants:
entity_type: node
type: comment
process:
entity_type: 'constants/entity_type'
field_name:
-
plugin: migration_lookup
source: type
migration: d7_comment_type
-
plugin: skip_on_empty
method: row
type: 'constants/type'
'settings/comment_type': '@field_name'
destination:
plugin: entity:field_storage_config
migration_dependencies:
required:
- d7_comment_type

View File

@@ -0,0 +1,71 @@
id: d7_comment_field_instance
label: Comment field instance configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: comment_type
constants:
entity_type: node
label: Comments
required: true
process:
entity_type: 'constants/entity_type'
label: 'constants/label'
required: 'constants/required'
field_name:
-
plugin: migration_lookup
source: type
migration: d7_comment_type
-
plugin: skip_on_empty
method: row
bundle: type
'default_value/0/status':
# We're using static_map instead of default_value otherwise if the source
# is 0, the default value of 1 would be used.
plugin: static_map
source: comment
map:
0: 0
1: 1
2: 2
default_value: 2
'settings/default_mode':
# We're using static_map instead of default_value otherwise if the source
# is 0, the default value of 1 would be used.
plugin: static_map
source: comment_default_mode
map:
0: 0
1: 1
default_value: 1
'settings/per_page':
plugin: default_value
source: comment_default_per_page
default_value: 50
'settings/anonymous':
plugin: default_value
source: comment_anonymous
default_value: 0
'settings/form_location':
plugin: default_value
source: comment_form_location
default_value: 0
'settings/preview':
# We're using static_map instead of default_value otherwise if the source
# is 0, the default value of 1 would be used.
plugin: static_map
source: comment_preview
map:
0: 0
1: 1
2: 2
default_value: 1
destination:
plugin: entity:field_config
migration_dependencies:
required:
- d7_node_type
- d7_comment_field

View File

@@ -0,0 +1,40 @@
id: d7_comment_type
label: Comment type
migration_tags:
- Drupal 7
- Configuration
source:
plugin: comment_type
constants:
entity_type: node
id_prefix: 'comment_node_'
label_suffix: 'comment'
process:
target_entity_type_id: 'constants/entity_type'
id:
-
plugin: concat
source:
- 'constants/id_prefix'
- type
-
plugin: static_map
bypass: true
# The Forum module provides its own comment type (comment_forum), which we
# want to reuse if it exists.
map:
comment_node_forum: comment_forum
-
plugin: make_unique_entity_field
entity_type: comment_type
field: id
length: 30
migrated: true
label:
plugin: concat
source:
- name
- 'constants/label_suffix'
delimiter: ' '
destination:
plugin: entity:comment_type

View File

@@ -0,0 +1,7 @@
finished:
6:
comment: comment
node: comment
7:
comment: comment
node: comment

View File

@@ -0,0 +1,162 @@
<?php
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the comment entity type.
*
* @see \Drupal\comment\Entity\Comment
*/
class CommentAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\comment\CommentInterface|\Drupal\user\EntityOwnerInterface $entity */
$comment_admin = $account->hasPermission('administer comments');
if ($operation == 'approve') {
return AccessResult::allowedIf($comment_admin && !$entity->isPublished())
->cachePerPermissions()
->addCacheableDependency($entity);
}
if ($comment_admin) {
$access = AccessResult::allowed()->cachePerPermissions();
return ($operation != 'view') ? $access : $access->andIf($entity->getCommentedEntity()->access($operation, $account, TRUE));
}
switch ($operation) {
case 'view':
$access_result = AccessResult::allowedIf($account->hasPermission('access comments') && $entity->isPublished())->cachePerPermissions()->addCacheableDependency($entity)
->andIf($entity->getCommentedEntity()->access($operation, $account, TRUE));
if (!$access_result->isAllowed()) {
$access_result->setReason("The 'access comments' permission is required and the comment must be published.");
}
return $access_result;
case 'update':
$access_result = AccessResult::allowedIf($account->id() && $account->id() == $entity->getOwnerId() && $entity->isPublished() && $account->hasPermission('edit own comments'))
->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity);
if (!$access_result->isAllowed()) {
$access_result->setReason("The 'edit own comments' permission is required, the user must be the comment author, and the comment must be published.");
}
return $access_result;
default:
// No opinion.
return AccessResult::neutral()->cachePerPermissions();
}
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
return AccessResult::allowedIfHasPermission($account, 'post comments');
}
/**
* {@inheritdoc}
*/
protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
if ($operation == 'edit') {
// Only users with the "administer comments" permission can edit
// administrative fields.
$administrative_fields = [
'uid',
'status',
'created',
'date',
];
if (in_array($field_definition->getName(), $administrative_fields, TRUE)) {
return AccessResult::allowedIfHasPermission($account, 'administer comments');
}
// No user can change read-only fields.
$read_only_fields = [
'hostname',
'changed',
'cid',
'thread',
];
// These fields can be edited during comment creation.
$create_only_fields = [
'comment_type',
'uuid',
'entity_id',
'entity_type',
'field_name',
'pid',
];
/** @var \Drupal\comment\CommentInterface|null $entity */
$entity = $items ? $items->getEntity() : NULL;
$commented_entity = $entity ? $entity->getCommentedEntity() : NULL;
if ($entity && $entity->isNew() && in_array($field_definition->getName(), $create_only_fields, TRUE)) {
$access_result = AccessResult::allowedIfHasPermission($account, 'post comments')
->addCacheableDependency($entity);
$comment_field_name = $entity->get('field_name')->value;
if ($commented_entity && $comment_field_name) {
// We are creating a new comment, user can edit create only fields if
// commenting is open.
$commenting_status = (int) $commented_entity->get($comment_field_name)->status;
$access_result = $access_result
->andIf(AccessResult::allowedIf($commenting_status !== CommentItemInterface::CLOSED))
->addCacheableDependency($commented_entity);
}
return $access_result;
}
// We are editing an existing comment - create only fields are now read
// only.
$read_only_fields = array_merge($read_only_fields, $create_only_fields);
if (in_array($field_definition->getName(), $read_only_fields, TRUE)) {
return AccessResult::forbidden();
}
// If the field is configured to accept anonymous contact details - admins
// can edit name, homepage and mail. Anonymous users can also fill in the
// fields on comment creation.
if (in_array($field_definition->getName(), ['name', 'mail', 'homepage'], TRUE)) {
if (!$items) {
// We cannot make a decision about access to edit these fields if we
// don't have any items and therefore cannot determine the Comment
// entity. In this case we err on the side of caution and prevent edit
// access.
return AccessResult::forbidden();
}
$is_name = $field_definition->getName() === 'name';
$anonymous_contact = $commented_entity->get($entity->getFieldName())->getFieldDefinition()->getSetting('anonymous');
$admin_access = AccessResult::allowedIfHasPermission($account, 'administer comments');
$anonymous_access = AccessResult::allowedIf($entity->isNew() && $account->isAnonymous() && ($anonymous_contact != CommentInterface::ANONYMOUS_MAYNOT_CONTACT || $is_name) && $account->hasPermission('post comments'))
->cachePerPermissions()
->addCacheableDependency($entity)
->addCacheableDependency($field_definition->getConfig($commented_entity->bundle()))
->addCacheableDependency($commented_entity);
return $admin_access->orIf($anonymous_access);
}
}
if ($operation == 'view') {
// Nobody has access to the hostname.
if ($field_definition->getName() == 'hostname') {
return AccessResult::forbidden();
}
// The mail field is hidden from non-admins.
if ($field_definition->getName() == 'mail') {
return AccessResult::allowedIfHasPermission($account, 'administer comments');
}
}
return parent::checkFieldAccess($operation, $field_definition, $account, $items);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Class to define the comment breadcrumb builder.
*/
class CommentBreadcrumbBuilder implements BreadcrumbBuilderInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs the CommentBreadcrumbBuilder.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public function applies(RouteMatchInterface $route_match) {
return $route_match->getRouteName() == 'comment.reply' && $route_match->getParameter('entity');
}
/**
* {@inheritdoc}
*/
public function build(RouteMatchInterface $route_match) {
$breadcrumb = new Breadcrumb();
$breadcrumb->addCacheContexts(['route']);
$breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
$entity = $route_match->getParameter('entity');
$breadcrumb->addLink(new Link($entity->label(), $entity->toUrl()));
$breadcrumb->addCacheableDependency($entity);
if (($pid = $route_match->getParameter('pid')) && ($comment = $this->entityTypeManager->getStorage('comment')->load($pid))) {
/** @var \Drupal\comment\CommentInterface $comment */
$breadcrumb->addCacheableDependency($comment);
// Display link to parent comment.
// @todo Clean-up permalink in https://www.drupal.org/node/2198041
$breadcrumb->addLink(new Link($comment->getSubject(), $comment->toUrl()));
}
return $breadcrumb;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Session\AccountInterface;
/**
* Defines an item list class for comment fields.
*/
class CommentFieldItemList extends FieldItemList {
/**
* {@inheritdoc}
*/
public function get($index) {
// The Field API only applies the "field default value" to newly created
// entities. In the specific case of the "comment status", though, we need
// this default value to be also applied for existing entities created
// before the comment field was added, which have no value stored for the
// field.
if ($index == 0 && empty($this->list)) {
$field_default_value = $this->getFieldDefinition()->getDefaultValue($this->getEntity());
return $this->appendItem($field_default_value[0]);
}
return parent::get($index);
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset) {
// For consistency with what happens in get(), we force offsetExists() to
// be TRUE for delta 0.
if ($offset === 0) {
return TRUE;
}
return parent::offsetExists($offset);
}
/**
* {@inheritdoc}
*/
public function access($operation = 'view', ?AccountInterface $account = NULL, $return_as_object = FALSE) {
if ($operation === 'edit') {
// Only users with administer comments permission can edit the comment
// status field.
$result = AccessResult::allowedIfHasPermission($account ?: \Drupal::currentUser(), 'administer comments');
return $return_as_object ? $result : $result->isAllowed();
}
if ($operation === 'view') {
// Only users with "post comments" or "access comments" permission can
// view the field value. The formatter,
// Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter,
// takes care of showing the thread and form based on individual
// permissions, so if a user only has post comments access, only the
// form will be shown and not the comments.
$result = AccessResult::allowedIfHasPermission($account ?: \Drupal::currentUser(), 'access comments')
->orIf(AccessResult::allowedIfHasPermission($account ?: \Drupal::currentUser(), 'post comments'));
return $return_as_object ? $result : $result->isAllowed();
}
return parent::access($operation, $account, $return_as_object);
}
}

View File

@@ -0,0 +1,442 @@
<?php
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base handler for comment forms.
*
* @internal
*/
class CommentForm extends ContentEntityForm {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('current_user'),
$container->get('renderer'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time'),
$container->get('entity_field.manager')
);
}
/**
* Constructs a new CommentForm.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface|null $entity_field_manager
* (optional) The entity field manager service.
*/
public function __construct(
EntityRepositoryInterface $entity_repository,
AccountInterface $current_user,
RendererInterface $renderer,
EntityTypeBundleInfoInterface $entity_type_bundle_info,
TimeInterface $time,
?EntityFieldManagerInterface $entity_field_manager = NULL,
) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$this->currentUser = $current_user;
$this->renderer = $renderer;
$this->entityFieldManager = $entity_field_manager ?: \Drupal::service('entity_field.manager');
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
/** @var \Drupal\comment\CommentInterface $comment */
$comment = $this->entity;
$entity = $this->entityTypeManager->getStorage($comment->getCommentedEntityTypeId())->load($comment->getCommentedEntityId());
$field_name = $comment->getFieldName();
$field_definition = $this->entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle())[$comment->getFieldName()];
$config = $this->config('user.settings');
// In several places within this function, we vary $form on:
// - The current user's permissions.
// - Whether the current user is authenticated or anonymous.
// - The 'user.settings' configuration.
// - The comment field's definition.
$form['#cache']['contexts'][] = 'user.permissions';
$form['#cache']['contexts'][] = 'user.roles:authenticated';
$this->renderer->addCacheableDependency($form, $config);
$this->renderer->addCacheableDependency($form, $field_definition->getConfig($entity->bundle()));
// Use #comment-form as unique jump target, regardless of entity type.
$form['#id'] = Html::getUniqueId('comment_form');
$form['#theme'] = ['comment_form__' . $entity->getEntityTypeId() . '__' . $entity->bundle() . '__' . $field_name, 'comment_form'];
$anonymous_contact = $field_definition->getSetting('anonymous');
$is_admin = $comment->id() && $this->currentUser->hasPermission('administer comments');
if (!$this->currentUser->isAuthenticated() && $anonymous_contact != CommentInterface::ANONYMOUS_MAYNOT_CONTACT) {
$form['#attached']['library'][] = 'core/drupal.form';
$form['#attributes']['data-user-info-from-browser'] = TRUE;
}
// If not replying to a comment, use our dedicated page callback for new
// Comments on entities.
if (!$comment->id() && !$comment->hasParentComment()) {
$form['#action'] = Url::fromRoute('comment.reply', ['entity_type' => $entity->getEntityTypeId(), 'entity' => $entity->id(), 'field_name' => $field_name])->toString();
}
$comment_preview = $form_state->get('comment_preview');
if (isset($comment_preview)) {
$form += $comment_preview;
}
$form['author'] = [];
// Display author information in a details element for comment moderators.
if ($is_admin) {
$form['author'] += [
'#type' => 'details',
'#title' => $this->t('Administration'),
];
}
// Prepare default values for form elements.
$author = '';
if ($is_admin) {
if (!$comment->getOwnerId()) {
$author = $comment->getAuthorName();
}
$status = $comment->isPublished() ? CommentInterface::PUBLISHED : CommentInterface::NOT_PUBLISHED;
if (empty($comment_preview)) {
$form['#title'] = $this->t('Edit comment %title', [
'%title' => $comment->getSubject(),
]);
}
}
else {
$status = ($this->currentUser->hasPermission('skip comment approval') ? CommentInterface::PUBLISHED : CommentInterface::NOT_PUBLISHED);
}
$date = '';
if ($comment->id()) {
$date = !empty($comment->date) ? $comment->date : DrupalDateTime::createFromTimestamp($comment->getCreatedTime());
}
// The uid field is only displayed when a user with the permission
// 'administer comments' is editing an existing comment from an
// authenticated user.
$owner = $comment->getOwner();
$form['author']['uid'] = [
'#type' => 'entity_autocomplete',
'#target_type' => 'user',
'#default_value' => $owner->isAnonymous() ? NULL : $owner,
// A comment can be made anonymous by leaving this field empty therefore
// there is no need to list them in the autocomplete.
'#selection_settings' => ['include_anonymous' => FALSE],
'#title' => $this->t('Authored by'),
'#description' => $this->t('Leave blank for %anonymous.', ['%anonymous' => $config->get('anonymous')]),
'#access' => $is_admin,
];
// The name field is displayed when an anonymous user is adding a comment or
// when a user with the permission 'administer comments' is editing an
// existing comment from an anonymous user.
$form['author']['name'] = [
'#type' => 'textfield',
'#title' => $is_admin ? $this->t('Name for @anonymous', ['@anonymous' => $config->get('anonymous')]) : $this->t('Your name'),
'#default_value' => $author,
'#required' => ($this->currentUser->isAnonymous() && $anonymous_contact == CommentInterface::ANONYMOUS_MUST_CONTACT),
'#maxlength' => 60,
'#access' => $this->currentUser->isAnonymous() || $is_admin,
'#size' => 30,
'#attributes' => [
'data-drupal-default-value' => $config->get('anonymous'),
],
];
if ($is_admin) {
// When editing a comment only display the name textfield if the uid field
// is empty.
$form['author']['name']['#states'] = [
'visible' => [
':input[name="uid"]' => ['empty' => TRUE],
],
];
}
// Add author email and homepage fields depending on the current user.
$form['author']['mail'] = [
'#type' => 'email',
'#title' => $this->t('Email'),
'#default_value' => $comment->getAuthorEmail(),
'#required' => ($this->currentUser->isAnonymous() && $anonymous_contact == CommentInterface::ANONYMOUS_MUST_CONTACT),
'#maxlength' => 64,
'#size' => 30,
'#description' => $this->t('The content of this field is kept private and will not be shown publicly.'),
'#access' => ($comment->getOwner()->isAnonymous() && $is_admin) || ($this->currentUser->isAnonymous() && $anonymous_contact != CommentInterface::ANONYMOUS_MAYNOT_CONTACT),
];
$form['author']['homepage'] = [
'#type' => 'url',
'#title' => $this->t('Homepage'),
'#default_value' => $comment->getHomepage(),
'#maxlength' => 255,
'#size' => 30,
'#access' => $is_admin || ($this->currentUser->isAnonymous() && $anonymous_contact != CommentInterface::ANONYMOUS_MAYNOT_CONTACT),
];
// Add administrative comment publishing options.
$form['author']['date'] = [
'#type' => 'datetime',
'#title' => $this->t('Authored on'),
'#default_value' => $date,
'#size' => 20,
'#access' => $is_admin,
];
$form['author']['status'] = [
'#type' => 'radios',
'#title' => $this->t('Status'),
'#default_value' => $status,
'#options' => [
CommentInterface::PUBLISHED => $this->t('Published'),
CommentInterface::NOT_PUBLISHED => $this->t('Not published'),
],
'#access' => $is_admin,
];
return parent::form($form, $form_state);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$element = parent::actions($form, $form_state);
/** @var \Drupal\comment\CommentInterface $comment */
$comment = $this->entity;
$entity = $comment->getCommentedEntity();
$field_definition = $this->entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle())[$comment->getFieldName()];
$preview_mode = $field_definition->getSetting('preview');
// No delete action on the comment form.
unset($element['delete']);
// Mark the submit action as the primary action, when it appears.
$element['submit']['#button_type'] = 'primary';
// Only show the save button if comment previews are optional or if we are
// already previewing the submission.
$element['submit']['#access'] = ($comment->id() && $this->currentUser->hasPermission('administer comments')) || $preview_mode != DRUPAL_REQUIRED || $form_state->get('comment_preview');
$element['preview'] = [
'#type' => 'submit',
'#value' => $this->t('Preview'),
'#access' => $preview_mode != DRUPAL_DISABLED,
'#submit' => ['::submitForm', '::preview'],
];
return $element;
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
/** @var \Drupal\comment\CommentInterface $comment */
$comment = parent::buildEntity($form, $form_state);
if (!$form_state->isValueEmpty('date') && $form_state->getValue('date') instanceof DrupalDateTime) {
$comment->setCreatedTime($form_state->getValue('date')->getTimestamp());
}
else {
$comment->setCreatedTime($this->time->getRequestTime());
}
// Empty author ID should revert to anonymous.
$author_id = $form_state->getValue('uid');
if ($comment->id() && $this->currentUser->hasPermission('administer comments')) {
// Admin can leave the author ID blank to revert to anonymous.
$author_id = $author_id ?: 0;
}
if (!is_null($author_id)) {
if ($author_id === 0 && $form['author']['name']['#access']) {
// Use the author name value when the form has access to the element and
// the author ID is anonymous.
$comment->setAuthorName($form_state->getValue('name'));
}
else {
// Ensure the author name is not set.
$comment->setAuthorName(NULL);
}
}
else {
$author_id = $this->currentUser->id();
}
$comment->setOwnerId($author_id);
// Validate the comment's subject. If not specified, extract from comment
// body.
if (trim($comment->getSubject()) == '') {
if ($comment->hasField('comment_body') && !$comment->comment_body->isEmpty()) {
// The body may be in any format, so:
// 1) Filter it into HTML
// 2) Strip out all HTML tags
// 3) Convert entities back to plain-text.
$comment_text = $comment->comment_body->processed;
$comment->setSubject(Unicode::truncate(trim(Html::decodeEntities(strip_tags($comment_text))), 29, TRUE, TRUE));
}
// Edge cases where the comment body is populated only by HTML tags will
// require a default subject.
if (trim($comment->getSubject()) == '') {
$comment->setSubject($this->t('(No subject)'));
}
}
return $comment;
}
/**
* {@inheritdoc}
*/
protected function getEditedFieldNames(FormStateInterface $form_state) {
return array_merge(['created', 'name'], parent::getEditedFieldNames($form_state));
}
/**
* {@inheritdoc}
*/
protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
// Manually flag violations of fields not handled by the form display.
foreach ($violations->getByField('created') as $violation) {
$form_state->setErrorByName('date', $violation->getMessage());
}
foreach ($violations->getByField('name') as $violation) {
$form_state->setErrorByName('name', $violation->getMessage());
}
parent::flagViolations($violations, $form, $form_state);
}
/**
* Form submission handler for the 'preview' action.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function preview(array &$form, FormStateInterface $form_state) {
$comment_preview = comment_preview($this->entity, $form_state);
$comment_preview['#title'] = $this->t('Preview comment');
$form_state->set('comment_preview', $comment_preview);
$form_state->setRebuild();
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$comment = $this->entity;
$entity = $comment->getCommentedEntity();
$is_new = $this->entity->isNew();
$field_name = $comment->getFieldName();
$uri = $entity->toUrl();
$logger = $this->logger('comment');
if ($this->currentUser->hasPermission('post comments') && ($this->currentUser->hasPermission('administer comments') || $entity->{$field_name}->status == CommentItemInterface::OPEN)) {
$comment->save();
$form_state->setValue('cid', $comment->id());
// Add a log entry.
$logger->info('Comment posted: %subject.', [
'%subject' => $comment->getSubject(),
'link' => Link::fromTextAndUrl(t('View'), $comment->toUrl()->setOption('fragment', 'comment-' . $comment->id()))->toString(),
]);
// Add an appropriate message upon submitting the comment form.
$this->messenger()->addStatus($this->getStatusMessage($comment, $is_new));
$query = [];
// Find the current display page for this comment.
$field_definition = $this->entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle())[$field_name];
$page = $this->entityTypeManager->getStorage('comment')->getDisplayOrdinal($comment, $field_definition->getSetting('default_mode'), $field_definition->getSetting('per_page'));
if ($page > 0) {
$query['page'] = $page;
}
// Redirect to the newly posted comment.
$uri->setOption('query', $query);
$uri->setOption('fragment', 'comment-' . $comment->id());
}
else {
$logger->warning('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', ['%subject' => $comment->getSubject()]);
$this->messenger()->addError($this->t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', ['%subject' => $comment->getSubject()]));
// Redirect the user to the entity they are commenting on.
}
$form_state->setRedirectUrl($uri);
}
/**
* Gets an appropriate status message when a comment is saved.
*
* @param \Drupal\comment\CommentInterface $comment
* The comment being saved.
* @param bool $is_new
* TRUE if a new comment is created. $comment->isNew() cannot be used here
* because the comment has already been saved by the time the message is
* rendered.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* A translatable string containing the appropriate status message.
*/
protected function getStatusMessage(CommentInterface $comment, bool $is_new): TranslatableMarkup {
if (!$comment->isPublished() && !$this->currentUser->hasPermission('administer comments')) {
return $this->t('Your comment has been queued for review by site administrators and will be published after approval.');
}
// Check whether the comment is new or not.
if ($is_new) {
return $this->t('Your comment has been posted.');
}
return $this->t('Your comment has been updated.');
}
}

View File

@@ -0,0 +1,246 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\user\EntityOwnerInterface;
use Drupal\Core\Entity\EntityChangedInterface;
/**
* Provides an interface defining a comment entity.
*/
interface CommentInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface, EntityPublishedInterface {
/**
* Comment is awaiting approval.
*/
const NOT_PUBLISHED = 0;
/**
* Comment is published.
*/
const PUBLISHED = 1;
/**
* Anonymous posters cannot enter their contact information.
*/
const ANONYMOUS_MAYNOT_CONTACT = 0;
/**
* Anonymous posters may leave their contact information.
*/
const ANONYMOUS_MAY_CONTACT = 1;
/**
* Anonymous posters are required to leave their contact information.
*/
const ANONYMOUS_MUST_CONTACT = 2;
/**
* Determines if this comment is a reply to another comment.
*
* @return bool
* TRUE if the comment has a parent comment otherwise FALSE.
*/
public function hasParentComment();
/**
* Returns the parent comment entity if this is a reply to a comment.
*
* @return \Drupal\comment\CommentInterface|null
* A comment entity of the parent comment or NULL if there is no parent.
*/
public function getParentComment();
/**
* Returns the entity to which the comment is attached.
*
* @return \Drupal\Core\Entity\FieldableEntityInterface|null
* The entity on which the comment is attached or NULL if the comment is an
* orphan.
*/
public function getCommentedEntity();
/**
* Returns the ID of the entity to which the comment is attached.
*
* @return int
* The ID of the entity to which the comment is attached.
*/
public function getCommentedEntityId();
/**
* Returns the type of the entity to which the comment is attached.
*
* @return string
* An entity type.
*/
public function getCommentedEntityTypeId();
/**
* Sets the field ID for which this comment is attached.
*
* @param string $field_name
* The field name through which the comment was added.
*
* @return $this
* The class instance that this method is called on.
*/
public function setFieldName($field_name);
/**
* Returns the name of the field the comment is attached to.
*
* @return string
* The name of the field the comment is attached to.
*/
public function getFieldName();
/**
* Returns the subject of the comment.
*
* @return string
* The subject of the comment.
*/
public function getSubject();
/**
* Sets the subject of the comment.
*
* @param string $subject
* The subject of the comment.
*
* @return $this
* The class instance that this method is called on.
*/
public function setSubject($subject);
/**
* Returns the comment author's name.
*
* For anonymous authors, this is the value as typed in the comment form.
*
* @return string
* The name of the comment author.
*/
public function getAuthorName();
/**
* Sets the name of the author of the comment.
*
* @param string $name
* A string containing the name of the author.
*
* @return $this
* The class instance that this method is called on.
*/
public function setAuthorName($name);
/**
* Returns the comment author's email address.
*
* For anonymous authors, this is the value as typed in the comment form.
*
* @return string
* The email address of the author of the comment.
*/
public function getAuthorEmail();
/**
* Returns the comment author's home page address.
*
* For anonymous authors, this is the value as typed in the comment form.
*
* @return string
* The homepage address of the author of the comment.
*/
public function getHomepage();
/**
* Sets the comment author's home page address.
*
* For anonymous authors, this is the value as typed in the comment form.
*
* @param string $homepage
* The homepage address of the author of the comment.
*
* @return $this
* The class instance that this method is called on.
*/
public function setHomepage($homepage);
/**
* Returns the comment author's hostname.
*
* @return string
* The hostname of the author of the comment.
*/
public function getHostname();
/**
* Sets the hostname of the author of the comment.
*
* @param string $hostname
* The hostname of the author of the comment.
*
* @return $this
* The class instance that this method is called on.
*/
public function setHostname($hostname);
/**
* Returns the time that the comment was created.
*
* @return int
* The timestamp of when the comment was created.
*/
public function getCreatedTime();
/**
* Sets the creation date of the comment.
*
* @param int $created
* The timestamp of when the comment was created.
*
* @return $this
* The class instance that this method is called on.
*/
public function setCreatedTime($created);
/**
* Returns the alphadecimal representation of the comment's place in a thread.
*
* @return string|null
* The alphadecimal representation of the comment's place in a thread. NULL
* is returned before a comment is saved.
*/
public function getThread();
/**
* Sets the alphadecimal representation of the comment's place in a thread.
*
* @param string $thread
* The alphadecimal representation of the comment's place in a thread.
*
* @return $this
* The class instance that this method is called on.
*/
public function setThread($thread);
/**
* Returns the permalink URL for this comment.
*
* @return \Drupal\Core\Url
*/
public function permalink();
/**
* Get the comment type id for this comment.
*
* @return string
* The id of the comment type.
*/
public function getTypeId();
}

View File

@@ -0,0 +1,240 @@
<?php
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Entity\EntityFormBuilderInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\Element\Link;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
/**
* Defines a service for comment #lazy_builder callbacks.
*/
class CommentLazyBuilders implements TrustedCallbackInterface {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity form builder service.
*
* @var \Drupal\Core\Entity\EntityFormBuilderInterface
*/
protected $entityFormBuilder;
/**
* Comment manager service.
*
* @var \Drupal\comment\CommentManagerInterface
*/
protected $commentManager;
/**
* Current logged in user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a new CommentLazyBuilders object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder
* The entity form builder service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current logged in user.
* @param \Drupal\comment\CommentManagerInterface $comment_manager
* The comment manager service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFormBuilderInterface $entity_form_builder, AccountInterface $current_user, CommentManagerInterface $comment_manager, ModuleHandlerInterface $module_handler, RendererInterface $renderer) {
$this->entityTypeManager = $entity_type_manager;
$this->entityFormBuilder = $entity_form_builder;
$this->currentUser = $current_user;
$this->commentManager = $comment_manager;
$this->moduleHandler = $module_handler;
$this->renderer = $renderer;
}
/**
* #lazy_builder callback; builds the comment form.
*
* @param string $commented_entity_type_id
* The commented entity type ID.
* @param string $commented_entity_id
* The commented entity ID.
* @param string $field_name
* The comment field name.
* @param string $comment_type_id
* The comment type ID.
*
* @return array
* A renderable array containing the comment form.
*/
public function renderForm($commented_entity_type_id, $commented_entity_id, $field_name, $comment_type_id) {
$values = [
'entity_type' => $commented_entity_type_id,
'entity_id' => $commented_entity_id,
'field_name' => $field_name,
'comment_type' => $comment_type_id,
'pid' => NULL,
];
$comment = $this->entityTypeManager->getStorage('comment')->create($values);
return $this->entityFormBuilder->getForm($comment);
}
/**
* #lazy_builder callback; builds a comment's links.
*
* @param string $comment_entity_id
* The comment entity ID.
* @param string $view_mode
* The view mode in which the comment entity is being viewed.
* @param string $langcode
* The language in which the comment entity is being viewed.
* @param bool $is_in_preview
* Whether the comment is currently being previewed.
*
* @return array
* A renderable array representing the comment links.
*/
public function renderLinks($comment_entity_id, $view_mode, $langcode, $is_in_preview) {
$links = [
'#theme' => 'links__comment',
'#pre_render' => [[Link::class, 'preRenderLinks']],
'#attributes' => ['class' => ['links', 'inline']],
];
if (!$is_in_preview) {
/** @var \Drupal\comment\CommentInterface $entity */
$entity = $this->entityTypeManager->getStorage('comment')->load($comment_entity_id);
if ($commented_entity = $entity->getCommentedEntity()) {
$links['comment'] = $this->buildLinks($entity, $commented_entity);
}
// Allow other modules to alter the comment links.
$hook_context = [
'view_mode' => $view_mode,
'langcode' => $langcode,
'commented_entity' => $commented_entity,
];
$this->moduleHandler->alter('comment_links', $links, $entity, $hook_context);
}
return $links;
}
/**
* Build the default links (reply, edit, delete ) for a comment.
*
* @param \Drupal\comment\CommentInterface $entity
* The comment object.
* @param \Drupal\Core\Entity\EntityInterface $commented_entity
* The entity to which the comment is attached.
*
* @return array
* An array that can be processed by Link::preRenderLinks().
*
* @see \Drupal\Core\Render\Element\Link::preRenderLinks()
*/
protected function buildLinks(CommentInterface $entity, EntityInterface $commented_entity) {
$links = [];
$status = $commented_entity->get($entity->getFieldName())->status;
if ($status == CommentItemInterface::OPEN) {
if ($entity->access('delete')) {
$links['comment-delete'] = [
'title' => t('Delete'),
'url' => $entity->toUrl('delete-form'),
];
}
if ($entity->access('update')) {
$links['comment-edit'] = [
'title' => t('Edit'),
'url' => $entity->toUrl('edit-form'),
];
}
$field_definition = $commented_entity->getFieldDefinition($entity->getFieldName());
if ($entity->access('create')
&& $field_definition->getSetting('default_mode') === CommentManagerInterface::COMMENT_MODE_THREADED) {
$links['comment-reply'] = [
'title' => t('Reply'),
'url' => Url::fromRoute('comment.reply', [
'entity_type' => $entity->getCommentedEntityTypeId(),
'entity' => $entity->getCommentedEntityId(),
'field_name' => $entity->getFieldName(),
'pid' => $entity->id(),
]),
];
}
if (!$entity->isPublished() && $entity->access('approve')) {
$links['comment-approve'] = [
'title' => t('Approve'),
'url' => Url::fromRoute('comment.approve', ['comment' => $entity->id()]),
];
}
if (empty($links) && $this->currentUser->isAnonymous()) {
$links['comment-forbidden']['title'] = $this->commentManager->forbiddenMessage($commented_entity, $entity->getFieldName());
}
}
// Add translations link for translation-enabled comment bundles.
if ($this->moduleHandler->moduleExists('content_translation') && $this->access($entity)->isAllowed()) {
$links['comment-translations'] = [
'title' => t('Translate'),
'url' => $entity->toUrl('drupal:content-translation-overview'),
];
}
return [
'#theme' => 'links__comment__comment',
// The "entity" property is specified to be present, so no need to check.
'#links' => $links,
'#attributes' => ['class' => ['links', 'inline']],
];
}
/**
* Wraps content_translation_translate_access.
*/
protected function access(EntityInterface $entity) {
return content_translation_translate_access($entity);
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['renderLinks', 'renderForm'];
}
}

View File

@@ -0,0 +1,224 @@
<?php
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Url;
/**
* Defines a class for building markup for comment links on a commented entity.
*
* Comment links include 'log in to post new comment', 'add new comment' etc.
*/
class CommentLinkBuilder implements CommentLinkBuilderInterface {
use StringTranslationTrait;
/**
* Current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Comment manager service.
*
* @var \Drupal\comment\CommentManagerInterface
*/
protected $commentManager;
/**
* Module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new CommentLinkBuilder object.
*
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user.
* @param \Drupal\comment\CommentManagerInterface $comment_manager
* Comment manager service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* Module handler service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* String translation service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(AccountInterface $current_user, CommentManagerInterface $comment_manager, ModuleHandlerInterface $module_handler, TranslationInterface $string_translation, EntityTypeManagerInterface $entity_type_manager) {
$this->currentUser = $current_user;
$this->commentManager = $comment_manager;
$this->moduleHandler = $module_handler;
$this->stringTranslation = $string_translation;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public function buildCommentedEntityLinks(FieldableEntityInterface $entity, array &$context) {
$entity_links = [];
$view_mode = $context['view_mode'];
if ($view_mode == 'search_index' || $view_mode == 'search_result' || $view_mode == 'print' || $view_mode == 'rss') {
// Do not add any links if the entity is displayed for:
// - search indexing.
// - constructing a search result excerpt.
// - print.
// - rss.
return [];
}
$fields = $this->commentManager->getFields($entity->getEntityTypeId());
foreach ($fields as $field_name => $detail) {
// Skip fields that the entity does not have.
if (!$entity->hasField($field_name)) {
continue;
}
$links = [];
$commenting_status = $entity->get($field_name)->status;
if ($commenting_status != CommentItemInterface::HIDDEN) {
// Entity has commenting status open or closed.
$field_definition = $entity->getFieldDefinition($field_name);
if ($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
// entity is open to new comments, and there currently are none.
if ($this->currentUser->hasPermission('access comments')) {
if (!empty($entity->get($field_name)->comment_count)) {
$links['comment-comments'] = [
'title' => $this->formatPlural($entity->get($field_name)->comment_count, '1 comment', '@count comments'),
'attributes' => ['title' => $this->t('Jump to the first comment.')],
'fragment' => 'comments',
'url' => $entity->toUrl(),
];
if ($this->moduleHandler->moduleExists('history')) {
$links['comment-new-comments'] = [
'title' => '',
'url' => Url::fromRoute('<current>'),
'attributes' => [
'class' => 'hidden',
'title' => $this->t('Jump to the first new comment.'),
'data-history-node-last-comment-timestamp' => $entity->get($field_name)->last_comment_timestamp,
'data-history-node-field-name' => $field_name,
],
];
}
}
}
// Provide a link to new comment form.
if ($commenting_status == CommentItemInterface::OPEN) {
$comment_form_location = $field_definition->getSetting('form_location');
if ($this->currentUser->hasPermission('post comments')) {
$links['comment-add'] = [
'title' => $this->t('Add new comment'),
'language' => $entity->language(),
'attributes' => ['title' => $this->t('Share your thoughts and opinions.')],
'fragment' => 'comment-form',
];
if ($comment_form_location == CommentItemInterface::FORM_SEPARATE_PAGE) {
$links['comment-add']['url'] = Url::fromRoute('comment.reply', [
'entity_type' => $entity->getEntityTypeId(),
'entity' => $entity->id(),
'field_name' => $field_name,
]);
}
else {
$links['comment-add'] += ['url' => $entity->toUrl()];
}
}
elseif ($this->currentUser->isAnonymous()) {
$links['comment-forbidden'] = [
'title' => $this->commentManager->forbiddenMessage($entity, $field_name),
];
}
}
}
else {
// 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.
if ($commenting_status == CommentItemInterface::OPEN) {
$comment_form_location = $field_definition->getSetting('form_location');
if ($this->currentUser->hasPermission('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 == CommentItemInterface::FORM_SEPARATE_PAGE || (!empty($entity->get($field_name)->comment_count) && $this->currentUser->hasPermission('access comments'))) {
$links['comment-add'] = [
'title' => $this->t('Add new comment'),
'attributes' => ['title' => $this->t('Share your thoughts and opinions.')],
'fragment' => 'comment-form',
];
if ($comment_form_location == CommentItemInterface::FORM_SEPARATE_PAGE) {
$links['comment-add']['url'] = Url::fromRoute('comment.reply', [
'entity_type' => $entity->getEntityTypeId(),
'entity' => $entity->id(),
'field_name' => $field_name,
]);
}
else {
$links['comment-add']['url'] = $entity->toUrl();
}
}
}
elseif ($this->currentUser->isAnonymous()) {
$links['comment-forbidden'] = [
'title' => $this->commentManager->forbiddenMessage($entity, $field_name),
];
}
}
}
}
if (!empty($links)) {
$entity_links['comment__' . $field_name] = [
'#theme' => 'links__entity__comment__' . $field_name,
'#links' => $links,
'#attributes' => ['class' => ['links', 'inline']],
];
if ($view_mode == 'teaser' && $this->moduleHandler->moduleExists('history') && $this->currentUser->isAuthenticated()) {
$entity_links['comment__' . $field_name]['#cache']['contexts'][] = 'user';
$entity_links['comment__' . $field_name]['#attached']['library'][] = 'comment/drupal.node-new-comments-link';
// Embed the metadata for the "X new comments" link (if any) on this
// entity.
$entity_links['comment__' . $field_name]['#attached']['drupalSettings']['history']['lastReadTimestamps'][$entity->id()] = history_read($entity->id());
$new_comments = $this->commentManager->getCountNewComments($entity);
if ($new_comments > 0) {
$page_number = $this->entityTypeManager
->getStorage('comment')
->getNewCommentPageNumber($entity->{$field_name}->comment_count, $new_comments, $entity, $field_name);
$query = $page_number ? ['page' => $page_number] : NULL;
$value = [
'new_comment_count' => (int) $new_comments,
'first_new_comment_link' => $entity->toUrl('canonical', [
'query' => $query,
'fragment' => 'new',
])->toString(),
];
$parents = ['comment', 'newCommentsLinks', $entity->getEntityTypeId(), $field_name, $entity->id()];
NestedArray::setValue($entity_links['comment__' . $field_name]['#attached']['drupalSettings'], $parents, $value);
}
}
}
}
return $entity_links;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* Defines an interface for building comment links on a commented entity.
*
* Comment links include 'log in to post new comment', 'add new comment' etc.
*/
interface CommentLinkBuilderInterface {
/**
* Builds links for the given entity.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* Entity for which the links are being built.
* @param array $context
* Array of context passed from the entity view builder.
*
* @return array
* Array of entity links.
*/
public function buildCommentedEntityLinks(FieldableEntityInterface $entity, array &$context);
}

View File

@@ -0,0 +1,239 @@
<?php
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
/**
* Comment manager contains common functions to manage comment fields.
*/
class CommentManager implements CommentManagerInterface {
use StringTranslationTrait;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Whether the \Drupal\user\RoleInterface::AUTHENTICATED_ID can post comments.
*
* @var bool
*/
protected $authenticatedCanPostComments;
/**
* The user settings config object.
*
* @var \Drupal\Core\Config\Config
*/
protected $userConfig;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Construct the CommentManager object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager service.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, TranslationInterface $string_translation, ModuleHandlerInterface $module_handler, AccountInterface $current_user, EntityFieldManagerInterface $entity_field_manager, EntityDisplayRepositoryInterface $entity_display_repository) {
$this->entityTypeManager = $entity_type_manager;
$this->userConfig = $config_factory->get('user.settings');
$this->stringTranslation = $string_translation;
$this->moduleHandler = $module_handler;
$this->currentUser = $current_user;
$this->entityFieldManager = $entity_field_manager;
$this->entityDisplayRepository = $entity_display_repository;
}
/**
* {@inheritdoc}
*/
public function getFields($entity_type_id) {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if (!$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
return [];
}
$map = $this->entityFieldManager->getFieldMapByFieldType('comment');
return $map[$entity_type_id] ?? [];
}
/**
* {@inheritdoc}
*/
public function addBodyField($comment_type_id) {
if (!FieldConfig::loadByName('comment', $comment_type_id, 'comment_body')) {
// Attaches the body field by default.
$field = $this->entityTypeManager->getStorage('field_config')->create([
'label' => 'Comment',
'bundle' => $comment_type_id,
'required' => TRUE,
'field_storage' => FieldStorageConfig::loadByName('comment', 'comment_body'),
]);
$field->save();
// Assign widget settings for the default form mode.
$this->entityDisplayRepository->getFormDisplay('comment', $comment_type_id)
->setComponent('comment_body', [
'type' => 'text_textarea',
])
->save();
// Assign display settings for the default view mode.
$this->entityDisplayRepository->getViewDisplay('comment', $comment_type_id)
->setComponent('comment_body', [
'label' => 'hidden',
'type' => 'text_default',
'weight' => 0,
])
->save();
}
}
/**
* {@inheritdoc}
*/
public function forbiddenMessage(EntityInterface $entity, $field_name) {
if (!isset($this->authenticatedCanPostComments)) {
// We only output a link if we are certain that users will get the
// permission to post comments by logging in.
$this->authenticatedCanPostComments = $this->entityTypeManager
->getStorage('user_role')
->load(RoleInterface::AUTHENTICATED_ID)
->hasPermission('post comments');
}
if ($this->authenticatedCanPostComments) {
// We cannot use the redirect.destination service here because these links
// sometimes appear on /node and taxonomy listing pages.
if ($entity->get($field_name)->getFieldDefinition()->getSetting('form_location') == CommentItemInterface::FORM_SEPARATE_PAGE) {
$comment_reply_parameters = [
'entity_type' => $entity->getEntityTypeId(),
'entity' => $entity->id(),
'field_name' => $field_name,
];
$destination = ['destination' => Url::fromRoute('comment.reply', $comment_reply_parameters, ['fragment' => 'comment-form'])->toString()];
}
else {
$destination = ['destination' => $entity->toUrl('canonical', ['fragment' => 'comment-form'])->toString()];
}
if ($this->userConfig->get('register') != UserInterface::REGISTER_ADMINISTRATORS_ONLY) {
// Users can register themselves.
return $this->t('<a href=":login">Log in</a> or <a href=":register">register</a> to post comments', [
':login' => Url::fromRoute('user.login', [], ['query' => $destination])->toString(),
':register' => Url::fromRoute('user.register', [], ['query' => $destination])->toString(),
]);
}
else {
// Only admins can add new users, no public registration.
return $this->t('<a href=":login">Log in</a> to post comments', [
':login' => Url::fromRoute('user.login', [], ['query' => $destination])->toString(),
]);
}
}
return '';
}
/**
* {@inheritdoc}
*/
public function getCountNewComments(EntityInterface $entity, $field_name = NULL, $timestamp = 0) {
// @todo Replace module handler with optional history service injection
// after https://www.drupal.org/node/2081585.
if ($this->currentUser->isAuthenticated() && $this->moduleHandler->moduleExists('history')) {
// Retrieve the timestamp at which the current user last viewed this entity.
if (!$timestamp) {
if ($entity->getEntityTypeId() == 'node') {
$timestamp = history_read($entity->id());
}
else {
$function = $entity->getEntityTypeId() . '_last_viewed';
if (function_exists($function)) {
$timestamp = $function($entity->id());
}
else {
// Default to 30 days ago.
// @todo Remove this else branch when we have a generic
// HistoryRepository service in https://www.drupal.org/node/3267011.
$timestamp = COMMENT_NEW_LIMIT;
}
}
}
$timestamp = ($timestamp > HISTORY_READ_LIMIT ? $timestamp : HISTORY_READ_LIMIT);
// Use the timestamp to retrieve the number of new comments.
$query = $this->entityTypeManager->getStorage('comment')->getQuery()
->accessCheck(TRUE)
->condition('entity_type', $entity->getEntityTypeId())
->condition('entity_id', $entity->id())
->condition('created', $timestamp, '>')
->condition('status', CommentInterface::PUBLISHED);
if ($field_name) {
// Limit to a particular field.
$query->condition('field_name', $field_name);
}
return $query->count()->execute();
}
return FALSE;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\EntityInterface;
/**
* Comment manager contains common functions to manage comment fields.
*/
interface CommentManagerInterface {
/**
* Comments are displayed in a flat list - expanded.
*/
const COMMENT_MODE_FLAT = 0;
/**
* Comments are displayed as a threaded list - expanded.
*/
const COMMENT_MODE_THREADED = 1;
/**
* Utility function to return an array of comment fields.
*
* @param string $entity_type_id
* The content entity type to which the comment fields are attached.
*
* @return array
* An array of comment field map definitions, keyed by field name. Each
* value is an array with two entries:
* - type: The field type.
* - bundles: The bundles in which the field appears, as an array with entity
* types as keys and the array of bundle names as values.
*/
public function getFields($entity_type_id);
/**
* Creates a comment_body field.
*
* @param string $comment_type
* The comment bundle.
*/
public function addBodyField($comment_type);
/**
* Provides a message if posting comments is forbidden.
*
* If authenticated users can post comments, a message is returned that
* prompts the anonymous user to log in (or register, if applicable) that
* redirects to entity comment form. Otherwise, no message is returned.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to which comments are attached to.
* @param string $field_name
* The field name on the entity to which comments are attached to.
*
* @return string
* HTML for a "you can't post comments" notice.
*/
public function forbiddenMessage(EntityInterface $entity, $field_name);
/**
* Returns the number of new comments available on a given entity for a user.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to which the comments are attached to.
* @param string $field_name
* (optional) The field_name to count comments for. Defaults to any field.
* @param int $timestamp
* (optional) Time to count from. Defaults to time of last user access the
* entity.
*
* @return int|false
* The number of new comments or FALSE if the user is not authenticated.
*/
public function getCountNewComments(EntityInterface $entity, $field_name = NULL, $timestamp = 0);
}

View File

@@ -0,0 +1,287 @@
<?php
namespace Drupal\comment;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\EntityOwnerInterface;
class CommentStatistics implements CommentStatisticsInterface {
/**
* The current database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The replica database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $databaseReplica;
/**
* The current logged in user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs the CommentStatistics service.
*
* @param \Drupal\Core\Database\Connection $database
* The active database connection.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current logged in user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\Component\Datetime\TimeInterface|null|\Drupal\Core\Database\Connection $time
* The time service.
* @param \Drupal\Core\Database\Connection|null $database_replica
* (Optional) the replica database connection.
*/
public function __construct(
Connection $database,
AccountInterface $current_user,
EntityTypeManagerInterface $entity_type_manager,
StateInterface $state,
protected TimeInterface|Connection|null $time = NULL,
?Connection $database_replica = NULL,
) {
$this->database = $database;
$this->currentUser = $current_user;
$this->entityTypeManager = $entity_type_manager;
$this->state = $state;
if (!$time || $time instanceof Connection) {
@trigger_error('Calling ' . __METHOD__ . '() without the $time argument is deprecated in drupal:10.3.0 and it will be the 4th argument in drupal:11.0.0. See https://www.drupal.org/node/3387233', E_USER_DEPRECATED);
if ($time instanceof Connection) {
$database_replica = $time;
}
$this->time = \Drupal::service(TimeInterface::class);
}
$this->databaseReplica = $database_replica ?: $database;
}
/**
* {@inheritdoc}
*/
public function read($entities, $entity_type, $accurate = TRUE) {
$connection = $accurate ? $this->database : $this->databaseReplica;
$stats = $connection->select('comment_entity_statistics', 'ces')
->fields('ces')
->condition('ces.entity_id', array_keys($entities), 'IN')
->condition('ces.entity_type', $entity_type)
->execute();
$statistics_records = [];
while ($entry = $stats->fetchObject()) {
$statistics_records[] = $entry;
}
return $statistics_records;
}
/**
* {@inheritdoc}
*/
public function delete(EntityInterface $entity) {
$this->database->delete('comment_entity_statistics')
->condition('entity_id', $entity->id())
->condition('entity_type', $entity->getEntityTypeId())
->execute();
}
/**
* {@inheritdoc}
*/
public function create(FieldableEntityInterface $entity, $fields) {
$query = $this->database->insert('comment_entity_statistics')
->fields([
'entity_id',
'entity_type',
'field_name',
'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.
if (!$entity->hasField($field_name)) {
continue;
}
// Get the user ID from the entity if it's set, or default to the
// currently logged in user.
$last_comment_uid = 0;
if ($entity instanceof EntityOwnerInterface) {
$last_comment_uid = $entity->getOwnerId();
}
if (!isset($last_comment_uid)) {
// Default to current user when entity does not implement
// EntityOwnerInterface or author is not set.
$last_comment_uid = $this->currentUser->id();
}
// Default to request time when entity does not have a changed property.
$last_comment_timestamp = $this->time->getRequestTime();
// @todo Make comment statistics language aware and add some tests. See
// https://www.drupal.org/node/2318875
if ($entity instanceof EntityChangedInterface) {
$last_comment_timestamp = $entity->getChangedTimeAcrossTranslations();
}
$query->values([
'entity_id' => $entity->id(),
'entity_type' => $entity->getEntityTypeId(),
'field_name' => $field_name,
'cid' => 0,
'last_comment_timestamp' => $last_comment_timestamp,
'last_comment_name' => NULL,
'last_comment_uid' => $last_comment_uid,
'comment_count' => 0,
]);
}
$query->execute();
}
/**
* {@inheritdoc}
*/
public function getMaximumCount($entity_type) {
return $this->database->query('SELECT MAX([comment_count]) FROM {comment_entity_statistics} WHERE [entity_type] = :entity_type', [':entity_type' => $entity_type])->fetchField();
}
/**
* {@inheritdoc}
*/
public function getRankingInfo() {
return [
'comments' => [
'title' => t('Number of comments'),
'join' => [
'type' => 'LEFT',
'table' => 'comment_entity_statistics',
'alias' => 'ces',
// Default to comment field as this is the most common use case for
// nodes.
'on' => "ces.entity_id = i.sid AND ces.entity_type = 'node' AND ces.field_name = 'comment'",
],
// Inverse law that maps the highest view count on the site to 1 and 0
// to 0. Note that the ROUND here is necessary for PostgreSQL and SQLite
// in order to ensure that the :comment_scale argument is treated as
// a numeric type, because the PostgreSQL PDO driver sometimes puts
// values in as strings instead of numbers in complex expressions like
// this.
'score' => '2.0 - 2.0 / (1.0 + ces.comment_count * (ROUND(:comment_scale, 4)))',
'arguments' => [':comment_scale' => \Drupal::state()->get('comment.node_comment_statistics_scale', 0)],
],
];
}
/**
* {@inheritdoc}
*/
public function update(CommentInterface $comment) {
// Allow bulk updates and inserts to temporarily disable the maintenance of
// the {comment_entity_statistics} table.
if (!$this->state->get('comment.maintain_entity_statistics')) {
return;
}
$query = $this->database->select('comment_field_data', 'c');
$query->addExpression('COUNT([cid])');
$count = $query->condition('c.entity_id', $comment->getCommentedEntityId())
->condition('c.entity_type', $comment->getCommentedEntityTypeId())
->condition('c.field_name', $comment->getFieldName())
->condition('c.status', CommentInterface::PUBLISHED)
->condition('default_langcode', 1)
->execute()
->fetchField();
if ($count > 0) {
// Comments exist.
$last_reply = $this->database->select('comment_field_data', 'c')
->fields('c', ['cid', 'name', 'changed', 'uid'])
->condition('c.entity_id', $comment->getCommentedEntityId())
->condition('c.entity_type', $comment->getCommentedEntityTypeId())
->condition('c.field_name', $comment->getFieldName())
->condition('c.status', CommentInterface::PUBLISHED)
->condition('default_langcode', 1)
->orderBy('c.created', 'DESC')
->range(0, 1)
->execute()
->fetchObject();
// Use merge here because entity could be created before comment field.
$this->database->merge('comment_entity_statistics')
->fields([
'cid' => $last_reply->cid,
'comment_count' => $count,
'last_comment_timestamp' => $last_reply->changed,
'last_comment_name' => $last_reply->uid ? '' : $last_reply->name,
'last_comment_uid' => $last_reply->uid,
])
->keys([
'entity_id' => $comment->getCommentedEntityId(),
'entity_type' => $comment->getCommentedEntityTypeId(),
'field_name' => $comment->getFieldName(),
])
->execute();
}
else {
// Comments do not exist.
$entity = $comment->getCommentedEntity();
// Get the user ID from the entity if it's set, or default to the
// currently logged in user.
if ($entity instanceof EntityOwnerInterface) {
$last_comment_uid = $entity->getOwnerId();
}
if (!isset($last_comment_uid)) {
// Default to current user when entity does not implement
// EntityOwnerInterface or author is not set.
$last_comment_uid = $this->currentUser->id();
}
$this->database->update('comment_entity_statistics')
->fields([
'cid' => 0,
'comment_count' => 0,
// Use the changed date of the entity if it's set, or default to
// request time.
'last_comment_timestamp' => ($entity instanceof EntityChangedInterface) ? $entity->getChangedTimeAcrossTranslations() : $this->time->getRequestTime(),
'last_comment_name' => '',
'last_comment_uid' => $last_comment_uid,
])
->condition('entity_id', $comment->getCommentedEntityId())
->condition('entity_type', $comment->getCommentedEntityTypeId())
->condition('field_name', $comment->getFieldName())
->execute();
}
// Reset the cache of the commented entity so that when the entity is loaded
// the next time, the statistics will be loaded again.
$this->entityTypeManager->getStorage($comment->getCommentedEntityTypeId())->resetCache([$comment->getCommentedEntityId()]);
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\EntityInterface;
/**
* Provides an interface for storing and retrieving comment statistics.
*/
interface CommentStatisticsInterface {
/**
* Returns an array of ranking information for hook_ranking().
*
* @return array
* Array of ranking information as expected by hook_ranking().
*
* @see hook_ranking()
* @see comment_ranking()
*/
public function getRankingInfo();
/**
* Read comment statistics records for an array of entities.
*
* @param \Drupal\Core\Entity\EntityInterface[] $entities
* Array of entities on which commenting is enabled, keyed by id
* @param string $entity_type
* The entity type of the passed entities.
* @param bool $accurate
* (optional) Indicates if results must be completely up to date. If set to
* FALSE, a replica database will used if available. Defaults to TRUE.
*
* @return object[]
* Array of statistics records.
*/
public function read($entities, $entity_type, $accurate = TRUE);
/**
* Delete comment statistics records for an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for which comment statistics should be deleted.
*/
public function delete(EntityInterface $entity);
/**
* Update or insert comment statistics records after a comment is added.
*
* @param \Drupal\comment\CommentInterface $comment
* The comment added or updated.
*/
public function update(CommentInterface $comment);
/**
* Find the maximum number of comments for the given entity type.
*
* Used to influence search rankings.
*
* @param string $entity_type
* The entity type to consider when fetching the maximum comment count for.
*
* @return int
* The maximum number of comments for and entity of the given type.
*
* @see comment_update_index()
*/
public function getMaximumCount($entity_type);
/**
* Insert an empty record for the given entity.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The created entity for which a statistics record is to be initialized.
* @param array $fields
* Array of comment field definitions for the given entity.
*/
public function create(FieldableEntityInterface $entity, $fields);
}

View File

@@ -0,0 +1,351 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\PagerSelectExtender;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the storage handler class for comments.
*
* This extends the Drupal\Core\Entity\Sql\SqlContentEntityStorage class,
* adding required special handling for comment entities.
*/
class CommentStorage extends SqlContentEntityStorage implements CommentStorageInterface {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a CommentStorage object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_info
* An array of entity info for the entity type.
* @param \Drupal\Core\Database\Connection $database
* The database connection to be used.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* Cache backend instance to use.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
* The memory cache.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeInterface $entity_info, Connection $database, EntityFieldManagerInterface $entity_field_manager, AccountInterface $current_user, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($entity_info, $database, $entity_field_manager, $cache, $language_manager, $memory_cache, $entity_type_bundle_info, $entity_type_manager);
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_info) {
return new static(
$entity_info,
$container->get('database'),
$container->get('entity_field.manager'),
$container->get('current_user'),
$container->get('cache.entity'),
$container->get('language_manager'),
$container->get('entity.memory_cache'),
$container->get('entity_type.bundle.info'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getMaxThread(CommentInterface $comment) {
$query = $this->database->select($this->getDataTable(), 'c')
->condition('entity_id', $comment->getCommentedEntityId())
->condition('field_name', $comment->getFieldName())
->condition('entity_type', $comment->getCommentedEntityTypeId())
->condition('default_langcode', 1);
$query->addExpression('MAX([thread])', 'thread');
return $query->execute()
->fetchField();
}
/**
* {@inheritdoc}
*/
public function getMaxThreadPerThread(CommentInterface $comment) {
$query = $this->database->select($this->getDataTable(), 'c')
->condition('entity_id', $comment->getCommentedEntityId())
->condition('field_name', $comment->getFieldName())
->condition('entity_type', $comment->getCommentedEntityTypeId())
->condition('thread', $comment->getParentComment()->getThread() . '.%', 'LIKE')
->condition('default_langcode', 1);
$query->addExpression('MAX([thread])', 'thread');
return $query->execute()
->fetchField();
}
/**
* {@inheritdoc}
*/
public function getDisplayOrdinal(CommentInterface $comment, $comment_mode, $divisor = 1) {
// Count how many comments (c1) are before $comment (c2) in display order.
// This is the 0-based display ordinal.
$data_table = $this->getDataTable();
$query = $this->database->select($data_table, 'c1');
$query->innerJoin($data_table, 'c2', '[c2].[entity_id] = [c1].[entity_id] AND [c2].[entity_type] = [c1].[entity_type] AND [c2].[field_name] = [c1].[field_name]');
$query->addExpression('COUNT(*)', 'count');
$query->condition('c2.cid', $comment->id());
if (!$this->currentUser->hasPermission('administer comments')) {
$query->condition('c1.status', CommentInterface::PUBLISHED);
}
if ($comment_mode == CommentManagerInterface::COMMENT_MODE_FLAT) {
// For rendering flat comments, cid is used for ordering comments due to
// unpredictable behavior with timestamp, so we make the same assumption
// here.
$query->condition('c1.cid', $comment->id(), '<');
}
else {
// For threaded comments, the c.thread column is used for ordering. We can
// use the sorting code for comparison, but must remove the trailing
// slash.
$query->where('SUBSTRING([c1].[thread], 1, (LENGTH([c1].[thread]) - 1)) < SUBSTRING([c2].[thread], 1, (LENGTH([c2].[thread]) - 1))');
}
$query->condition('c1.default_langcode', 1);
$query->condition('c2.default_langcode', 1);
$ordinal = $query->execute()->fetchField();
return ($divisor > 1) ? floor($ordinal / $divisor) : $ordinal;
}
/**
* {@inheritdoc}
*/
public function getNewCommentPageNumber($total_comments, $new_comments, FieldableEntityInterface $entity, $field_name) {
$field = $entity->getFieldDefinition($field_name);
$comments_per_page = $field->getSetting('per_page');
$data_table = $this->getDataTable();
if ($total_comments <= $comments_per_page) {
// Only one page of comments.
$count = 0;
}
elseif ($field->getSetting('default_mode') == CommentManagerInterface::COMMENT_MODE_FLAT) {
// Flat comments.
$count = $total_comments - $new_comments;
}
else {
// Threaded comments.
// 1. Find all the threads with a new comment.
$unread_threads_query = $this->database->select($data_table, 'comment')
->fields('comment', ['thread'])
->condition('entity_id', $entity->id())
->condition('entity_type', $entity->getEntityTypeId())
->condition('field_name', $field_name)
->condition('status', CommentInterface::PUBLISHED)
->condition('default_langcode', 1)
->orderBy('created', 'DESC')
->orderBy('cid', 'DESC')
->range(0, $new_comments);
// 2. Find the first thread.
$first_thread_query = $this->database->select($unread_threads_query, 'thread');
$first_thread_query->addExpression('SUBSTRING([thread], 1, (LENGTH([thread]) - 1))', 'torder');
$first_thread = $first_thread_query
->fields('thread', ['thread'])
->orderBy('torder')
->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.
$count = $this->database->query('SELECT COUNT(*) FROM {' . $data_table . '} WHERE [entity_id] = :entity_id
AND [entity_type] = :entity_type
AND [field_name] = :field_name
AND [status] = :status
AND SUBSTRING([thread], 1, (LENGTH([thread]) - 1)) < :thread
AND [default_langcode] = 1', [
':status' => CommentInterface::PUBLISHED,
':entity_id' => $entity->id(),
':field_name' => $field_name,
':entity_type' => $entity->getEntityTypeId(),
':thread' => $first_thread,
]
)->fetchField();
}
return $comments_per_page > 0 ? (int) ($count / $comments_per_page) : 0;
}
/**
* {@inheritdoc}
*/
public function getChildCids(array $comments) {
return $this->database->select($this->getDataTable(), 'c')
->fields('c', ['cid'])
->condition('pid', array_keys($comments), 'IN')
->condition('default_langcode', 1)
->execute()
->fetchCol();
}
/**
* {@inheritdoc}
*
* 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.
*/
public function loadThread(EntityInterface $entity, $field_name, $mode, $comments_per_page = 0, $pager_id = 0) {
$data_table = $this->getDataTable();
$query = $this->database->select($data_table, 'c');
$query->addField('c', 'cid');
$query
->condition('c.entity_id', $entity->id())
->condition('c.entity_type', $entity->getEntityTypeId())
->condition('c.field_name', $field_name)
->condition('c.default_langcode', 1)
->addTag('entity_access')
->addTag('comment_filter')
->addMetaData('base_table', 'comment')
->addMetaData('entity', $entity)
->addMetaData('field_name', $field_name);
if ($comments_per_page) {
$query = $query->extend(PagerSelectExtender::class)
->limit($comments_per_page);
if ($pager_id) {
$query->element($pager_id);
}
$count_query = $this->database->select($data_table, 'c');
$count_query->addExpression('COUNT(*)');
$count_query
->condition('c.entity_id', $entity->id())
->condition('c.entity_type', $entity->getEntityTypeId())
->condition('c.field_name', $field_name)
->condition('c.default_langcode', 1)
->addTag('entity_access')
->addTag('comment_filter')
->addMetaData('base_table', 'comment')
->addMetaData('entity', $entity)
->addMetaData('field_name', $field_name);
$query->setCountQuery($count_query);
}
if (!$this->currentUser->hasPermission('administer comments')) {
$query->condition('c.status', CommentInterface::PUBLISHED);
if ($comments_per_page) {
$count_query->condition('c.status', CommentInterface::PUBLISHED);
}
}
if ($mode == CommentManagerInterface::COMMENT_MODE_FLAT) {
$query->orderBy('c.cid', 'ASC');
}
else {
// See comment above. Analysis reveals that this doesn't cost too much. It
// scales much better than having the whole comment structure.
$query->addExpression('SUBSTRING([c].[thread], 1, (LENGTH([c].[thread]) - 1))', 'torder');
$query->orderBy('torder', 'ASC');
}
$cids = $query->execute()->fetchCol();
$comments = [];
if ($cids) {
$comments = $this->loadMultiple($cids);
}
return $comments;
}
/**
* {@inheritdoc}
*/
public function getUnapprovedCount() {
return $this->database->select($this->getDataTable(), 'c')
->condition('status', CommentInterface::NOT_PUBLISHED, '=')
->condition('default_langcode', 1)
->countQuery()
->execute()
->fetchField();
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* Defines an interface for comment entity storage classes.
*/
interface CommentStorageInterface extends ContentEntityStorageInterface {
/**
* Gets the maximum encoded thread value for the top level comments.
*
* @param \Drupal\comment\CommentInterface $comment
* A comment entity.
*
* @return string|null
* The maximum encoded thread value among the top level comments of the
* node $comment belongs to. NULL is returned when the commented entity has
* no comments.
*/
public function getMaxThread(CommentInterface $comment);
/**
* Gets the maximum encoded thread value for the children of this comment.
*
* @param \Drupal\comment\CommentInterface $comment
* A comment entity.
*
* @return string|null
* The maximum encoded thread value among all replies of $comment. NULL is
* returned when the commented entity has no comments.
*/
public function getMaxThreadPerThread(CommentInterface $comment);
/**
* Calculates the page number for the first new comment.
*
* @param int $total_comments
* The total number of comments that the entity has.
* @param int $new_comments
* The number of new comments that the entity has.
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity to which the comments belong.
* @param string $field_name
* The field name on the entity to which comments are attached.
*
* @return array|null
* The page number where first new comment appears. (First page returns 0.)
*/
public function getNewCommentPageNumber($total_comments, $new_comments, FieldableEntityInterface $entity, $field_name);
/**
* Gets the display ordinal or page number for a comment.
*
* @param \Drupal\comment\CommentInterface $comment
* The comment to use as a reference point.
* @param int $comment_mode
* The comment display mode: CommentManagerInterface::COMMENT_MODE_FLAT or
* CommentManagerInterface::COMMENT_MODE_THREADED.
* @param int $divisor
* Defaults to 1, which returns the display ordinal for a comment. If the
* number of comments per page is provided, the returned value will be the
* page number. (The return value will be divided by $divisor.)
*
* @return int
* The display ordinal or page number for the comment. It is 0-based, so
* will represent the number of items before the given comment/page.
*/
public function getDisplayOrdinal(CommentInterface $comment, $comment_mode, $divisor = 1);
/**
* Gets the comment ids of the passed comment entities' children.
*
* @param \Drupal\comment\CommentInterface[] $comments
* An array of comment entities keyed by their ids.
*
* @return array
* The entity ids of the passed comment entities' children as an array.
*/
public function getChildCids(array $comments);
/**
* Retrieves comments for a thread, sorted in an order suitable for display.
*
* @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
* The comment display mode: CommentManagerInterface::COMMENT_MODE_FLAT or
* CommentManagerInterface::COMMENT_MODE_THREADED.
* @param int $comments_per_page
* (optional) The amount of comments to display per page.
* Defaults to 0, which means show all comments.
* @param int $pager_id
* (optional) Pager id to use in case of multiple pagers on the one page.
* Defaults to 0; is only used when $comments_per_page is greater than zero.
*
* @return array
* Ordered array of comment objects, keyed by comment id.
*/
public function loadThread(EntityInterface $entity, $field_name, $mode, $comments_per_page = 0, $pager_id = 0);
/**
* Returns the number of unapproved comments.
*
* @return int
* The number of unapproved comments.
*/
public function getUnapprovedCount();
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\RequiredFieldStorageDefinitionInterface;
/**
* Defines the comment schema handler.
*/
class CommentStorageSchema extends SqlContentEntityStorageSchema {
/**
* {@inheritdoc}
*/
protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
$schema = parent::getEntitySchema($entity_type, $reset);
if ($data_table = $this->storage->getDataTable()) {
$schema[$data_table]['indexes'] += [
'comment__status_pid' => ['pid', 'status'],
'comment__num_new' => [
'entity_id',
'entity_type',
'comment_type',
'status',
'created',
'cid',
'thread',
],
'comment__entity_langcode' => [
'entity_id',
'entity_type',
'comment_type',
'default_langcode',
],
];
}
return $schema;
}
/**
* {@inheritdoc}
*/
protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
$schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
$field_name = $storage_definition->getName();
if ($table_name == 'comment_field_data') {
// Remove unneeded indexes.
unset($schema['indexes']['comment_field__pid__target_id']);
unset($schema['indexes']['comment_field__entity_id__target_id']);
switch ($field_name) {
case 'thread':
// Improves the performance of the comment__num_new index defined
// in getEntitySchema().
$schema['fields'][$field_name]['not null'] = TRUE;
break;
case 'entity_type':
case 'field_name':
assert($storage_definition instanceof RequiredFieldStorageDefinitionInterface);
if ($storage_definition->isStorageRequired()) {
// The 'entity_type' and 'field_name' are required so they also need
// to be marked as NOT NULL.
$schema['fields'][$field_name]['not null'] = TRUE;
}
break;
case 'created':
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
break;
case 'uid':
$this->addSharedTableFieldForeignKey($storage_definition, $schema, 'users', 'uid');
}
}
return $schema;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\EntityInterface;
use Drupal\content_translation\ContentTranslationHandler;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines the translation handler for comments.
*/
class CommentTranslationHandler extends ContentTranslationHandler {
/**
* {@inheritdoc}
*/
public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity) {
parent::entityFormAlter($form, $form_state, $entity);
if (isset($form['content_translation'])) {
// We do not need to show these values on comment forms: they inherit the
// basic comment property values.
$form['content_translation']['status']['#access'] = FALSE;
$form['content_translation']['name']['#access'] = FALSE;
$form['content_translation']['created']['#access'] = FALSE;
}
}
/**
* {@inheritdoc}
*/
protected function entityFormTitle(EntityInterface $entity) {
return t('Edit comment @subject', ['@subject' => $entity->label()]);
}
/**
* {@inheritdoc}
*/
public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, FormStateInterface $form_state) {
if ($form_state->hasValue('content_translation')) {
$translation = &$form_state->getValue('content_translation');
/** @var \Drupal\comment\CommentInterface $entity */
$translation['status'] = $entity->isPublished();
$translation['name'] = $entity->getAuthorName();
}
parent::entityFormEntityBuild($entity_type, $entity, $form, $form_state);
}
}

View File

@@ -0,0 +1,200 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\language\Entity\ContentLanguageSettings;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base form handler for comment type edit forms.
*
* @internal
*/
class CommentTypeForm extends EntityForm {
/**
* Entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The comment manager.
*
* @var \Drupal\comment\CommentManagerInterface
*/
protected $commentManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('logger.factory')->get('comment'),
$container->get('comment.manager')
);
}
/**
* Constructs a CommentTypeFormController.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\comment\CommentManagerInterface $comment_manager
* The comment manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, LoggerInterface $logger, CommentManagerInterface $comment_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->logger = $logger;
$this->commentManager = $comment_manager;
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$comment_type = $this->entity;
if ($this->operation === 'edit') {
$form['#title'] = $this->t('Edit %label comment type', ['%label' => $comment_type->label()]);
}
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $comment_type->label(),
'#description' => $this->t('The human-readable name for this comment type, displayed on the <em>Comment types</em> page.'),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $comment_type->id(),
'#machine_name' => [
'exists' => '\Drupal\comment\Entity\CommentType::load',
],
'#description' => $this->t('Unique machine-readable name: lowercase letters, numbers, and underscores only.'),
'#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
'#disabled' => !$comment_type->isNew(),
];
$form['description'] = [
'#type' => 'textarea',
'#default_value' => $comment_type->getDescription(),
'#description' => $this->t('Displays on the <em>Comment types</em> page.'),
'#title' => $this->t('Description'),
];
if ($comment_type->isNew()) {
$options = [];
// Only expose entities that have field UI enabled, only those can
// get comment fields added in the UI. Also, ensure to include only
// entities that have integer id.
foreach ($this->entityTypeManager->getDefinitions() as $entity_type) {
if ($this->entityTypeSupportsComments($entity_type)) {
if ($entity_type->get('field_ui_base_route')) {
$options[$entity_type->id()] = $entity_type->getLabel();
}
}
}
$form['target_entity_type_id'] = [
'#type' => 'select',
'#default_value' => $comment_type->getTargetEntityTypeId(),
'#title' => $this->t('Target entity type'),
'#required' => TRUE,
'#empty_value' => '_none',
'#options' => $options,
'#description' => $this->t('The target entity type can not be changed after the comment type has been created.'),
];
}
else {
$form['target_entity_type_id_display'] = [
'#type' => 'item',
'#markup' => $this->entityTypeManager->getDefinition($comment_type->getTargetEntityTypeId())->getLabel(),
'#title' => $this->t('Target entity type'),
];
}
if ($this->moduleHandler->moduleExists('content_translation')) {
$form['language'] = [
'#type' => 'details',
'#title' => $this->t('Language settings'),
'#group' => 'additional_settings',
];
$language_configuration = ContentLanguageSettings::loadByEntityTypeBundle('comment', $comment_type->id());
$form['language']['language_configuration'] = [
'#type' => 'language_configuration',
'#entity_information' => [
'entity_type' => 'comment',
'bundle' => $comment_type->id(),
],
'#default_value' => $language_configuration,
];
$form['#submit'][] = 'language_configuration_element_submit';
}
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
];
return $form;
}
/**
* Wraps _comment_entity_uses_integer_id().
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* Entity type being tested.
*
* @return bool
* TRUE if entity-type uses integer IDs.
*/
protected function entityTypeSupportsComments(EntityTypeInterface $entity_type) {
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && _comment_entity_uses_integer_id($entity_type->id());
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$comment_type = $this->entity;
$status = $comment_type->save();
$edit_link = $this->entity->toLink($this->t('Edit'), 'edit-form')->toString();
if ($status == SAVED_UPDATED) {
$this->messenger()->addStatus($this->t('Comment type %label has been updated.', ['%label' => $comment_type->label()]));
$this->logger->notice('Comment type %label has been updated.', ['%label' => $comment_type->label(), 'link' => $edit_link]);
}
else {
$this->commentManager->addBodyField($comment_type->id());
$this->messenger()->addStatus($this->t('Comment type %label has been added.', ['%label' => $comment_type->label()]));
$this->logger->notice('Comment type %label has been added.', ['%label' => $comment_type->label(), 'link' => $edit_link]);
}
$form_state->setRedirectUrl($comment_type->toUrl('collection'));
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining a comment type entity.
*/
interface CommentTypeInterface extends ConfigEntityInterface {
/**
* Returns the comment type description.
*
* @return string
* The comment-type description.
*/
public function getDescription();
/**
* Sets the description of the comment type.
*
* @param string $description
* The new description.
*
* @return $this
*/
public function setDescription($description);
/**
* Gets the target entity type id for this comment type.
*
* @return string
* The target entity type id.
*/
public function getTargetEntityTypeId();
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class to build a listing of comment type entities.
*
* @see \Drupal\comment\Entity\CommentType
*/
class CommentTypeListBuilder extends ConfigEntityListBuilder {
/**
* Constructs a new CommentTypeListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, protected EntityTypeManagerInterface $entityTypeManager) {
parent::__construct($entity_type, $storage);
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('entity_type.manager'),
);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
// Place the edit operation after the operations added by field_ui.module
// which have the weights 15, 20, 25.
if (isset($operations['edit'])) {
$operations['edit']['weight'] = 30;
}
return $operations;
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['type'] = t('Comment type');
$header['description'] = t('Description');
$header['target'] = t('Target entity type');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
assert($entity instanceof CommentTypeInterface);
$entity_type = $this->entityTypeManager->getDefinition($entity->getTargetEntityTypeId());
$row['type'] = $entity->label();
$row['description']['data'] = ['#markup' => $entity->getDescription()];
$row['target'] = $entity_type->getLabel();
return $row + parent::buildRow($entity);
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityViewBuilder;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Theme\Registry;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* View builder handler for comments.
*/
class CommentViewBuilder extends EntityViewBuilder {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new CommentViewBuilder.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Theme\Registry $theme_registry
* The theme registry.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeInterface $entity_type, EntityRepositoryInterface $entity_repository, LanguageManagerInterface $language_manager, AccountInterface $current_user, Registry $theme_registry, EntityDisplayRepositoryInterface $entity_display_repository, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($entity_type, $entity_repository, $language_manager, $theme_registry, $entity_display_repository);
$this->currentUser = $current_user;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity.repository'),
$container->get('language_manager'),
$container->get('current_user'),
$container->get('theme.registry'),
$container->get('entity_display.repository'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
protected function getBuildDefaults(EntityInterface $entity, $view_mode) {
$build = parent::getBuildDefaults($entity, $view_mode);
/** @var \Drupal\comment\CommentInterface $entity */
// Store a threading field setting to use later in self::buildComponents().
$commented_entity = $entity->getCommentedEntity();
$build['#comment_threaded'] =
is_null($commented_entity)
|| $commented_entity->getFieldDefinition($entity->getFieldName())
->getSetting('default_mode') === CommentManagerInterface::COMMENT_MODE_THREADED;
// If threading is enabled, don't render cache individual comments, but do
// keep the cacheability metadata, so it can bubble up.
if ($build['#comment_threaded']) {
unset($build['#cache']['keys']);
}
return $build;
}
/**
* {@inheritdoc}
*
* In addition to modifying the content key on entities, this implementation
* will also set the comment entity key which all comments carry.
*
* @throws \InvalidArgumentException
* Thrown when a comment is attached to an entity that no longer exists.
*/
public function buildComponents(array &$build, array $entities, array $displays, $view_mode) {
/** @var \Drupal\comment\CommentInterface[] $entities */
if (empty($entities)) {
return;
}
// Pre-load associated users into cache to leverage multiple loading.
$uids = [];
foreach ($entities as $entity) {
$uids[] = $entity->getOwnerId();
}
$this->entityTypeManager->getStorage('user')->loadMultiple(array_unique($uids));
parent::buildComponents($build, $entities, $displays, $view_mode);
// A counter to track the indentation level.
$current_indent = 0;
$attach_history = $this->moduleHandler->moduleExists('history') && $this->currentUser->isAuthenticated();
foreach ($entities as $id => $entity) {
if ($build[$id]['#comment_threaded']) {
$comment_indent = count(explode('.', (string) $entity->getThread())) - 1;
if ($comment_indent > $current_indent) {
// Set 1 to indent this comment from the previous one (its parent).
// Set only one extra level of indenting even if the difference in
// depth is higher.
$build[$id]['#comment_indent'] = 1;
$current_indent++;
}
else {
// Set zero if this comment is on the same level as the previous one
// or negative value to point an amount indents to close.
$build[$id]['#comment_indent'] = $comment_indent - $current_indent;
$current_indent = $comment_indent;
}
}
// Commented entities already loaded after self::getBuildDefaults().
$commented_entity = $entity->getCommentedEntity();
// Set defaults if the commented_entity does not exist.
$bundle = $commented_entity ? $commented_entity->bundle() : '';
$is_node = $commented_entity ? $commented_entity->getEntityTypeId() === 'node' : NULL;
$build[$id]['#entity'] = $entity;
$build[$id]['#theme'] = 'comment__' . $entity->getFieldName() . '__' . $bundle;
$display = $displays[$entity->bundle()];
if ($display->getComponent('links')) {
$build[$id]['links'] = [
'#lazy_builder' => [
'comment.lazy_builders:renderLinks',
[
$entity->id(),
$view_mode,
$entity->language()->getId(),
!empty($entity->in_preview),
],
],
'#create_placeholder' => TRUE,
];
}
if (!isset($build[$id]['#attached'])) {
$build[$id]['#attached'] = [];
}
$build[$id]['#attached']['library'][] = 'comment/drupal.comment-by-viewer';
if ($attach_history && $is_node) {
$build[$id]['#attached']['library'][] = 'comment/drupal.comment-new-indicator';
// Embed the metadata for the comment "new" indicators on this node.
$build[$id]['history'] = [
'#lazy_builder' => ['\Drupal\history\HistoryRenderCallback::lazyBuilder', [$commented_entity->id()]],
'#create_placeholder' => TRUE,
];
}
}
if ($build[$id]['#comment_threaded']) {
// The final comment must close up some hanging divs.
$build[$id]['#comment_indent_final'] = $current_indent;
}
}
/**
* {@inheritdoc}
*/
protected function alterBuild(array &$build, EntityInterface $comment, EntityViewDisplayInterface $display, $view_mode) {
parent::alterBuild($build, $comment, $display, $view_mode);
if (empty($comment->in_preview)) {
$prefix = '';
// Add indentation div or close open divs as needed.
if ($build['#comment_threaded']) {
$prefix .= $build['#comment_indent'] <= 0 ? str_repeat('</div>', abs($build['#comment_indent'])) : "\n" . '<div class="indented">';
}
$build['#prefix'] = $prefix;
// Close all open divs.
if (!empty($build['#comment_indent_final'])) {
$build['#suffix'] = str_repeat('</div>', $build['#comment_indent_final']);
}
}
}
}

View File

@@ -0,0 +1,397 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\views\EntityViewsData;
/**
* Provides views data for the comment entity type.
*/
class CommentViewsData extends EntityViewsData {
/**
* {@inheritdoc}
*/
public function getViewsData() {
$data = parent::getViewsData();
$data['comment_field_data']['table']['base']['help'] = $this->t('Comments are responses to content.');
$data['comment_field_data']['table']['base']['access query tag'] = 'comment_access';
$data['comment_field_data']['table']['wizard_id'] = 'comment';
$data['comment_field_data']['subject']['title'] = $this->t('Title');
$data['comment_field_data']['subject']['help'] = $this->t('The title of the comment.');
$data['comment_field_data']['subject']['field']['default_formatter'] = 'comment_permalink';
$data['comment_field_data']['name']['title'] = $this->t('Author');
$data['comment_field_data']['name']['help'] = $this->t("The name of the comment's author. Can be rendered as a link to the author's homepage.");
$data['comment_field_data']['name']['field']['default_formatter'] = 'comment_username';
$data['comment_field_data']['homepage']['title'] = $this->t("Author's website");
$data['comment_field_data']['homepage']['help'] = $this->t("The website address of the comment's author. Can be rendered as a link. Will be empty if the author is a registered user.");
$data['comment_field_data']['mail']['help'] = $this->t('Email of user that posted the comment. Will be empty if the author is a registered user.');
$data['comment_field_data']['created']['title'] = $this->t('Post date');
$data['comment_field_data']['created']['help'] = $this->t('Date and time of when the comment was created.');
$data['comment_field_data']['created_fulldata'] = [
'title' => $this->t('Created date'),
'help' => $this->t('Date in the form of CCYYMMDD.'),
'argument' => [
'field' => 'created',
'id' => 'date_fulldate',
],
];
$data['comment_field_data']['created_year_month'] = [
'title' => $this->t('Created year + month'),
'help' => $this->t('Date in the form of YYYYMM.'),
'argument' => [
'field' => 'created',
'id' => 'date_year_month',
],
];
$data['comment_field_data']['created_year'] = [
'title' => $this->t('Created year'),
'help' => $this->t('Date in the form of YYYY.'),
'argument' => [
'field' => 'created',
'id' => 'date_year',
],
];
$data['comment_field_data']['created_month'] = [
'title' => $this->t('Created month'),
'help' => $this->t('Date in the form of MM (01 - 12).'),
'argument' => [
'field' => 'created',
'id' => 'date_month',
],
];
$data['comment_field_data']['created_day'] = [
'title' => $this->t('Created day'),
'help' => $this->t('Date in the form of DD (01 - 31).'),
'argument' => [
'field' => 'created',
'id' => 'date_day',
],
];
$data['comment_field_data']['created_week'] = [
'title' => $this->t('Created week'),
'help' => $this->t('Date in the form of WW (01 - 53).'),
'argument' => [
'field' => 'created',
'id' => 'date_week',
],
];
$data['comment_field_data']['changed']['title'] = $this->t('Updated date');
$data['comment_field_data']['changed']['help'] = $this->t('Date and time of when the comment was last updated.');
$data['comment_field_data']['changed_fulldata'] = [
'title' => $this->t('Changed date'),
'help' => $this->t('Date in the form of CCYYMMDD.'),
'argument' => [
'field' => 'changed',
'id' => 'date_fulldate',
],
];
$data['comment_field_data']['changed_year_month'] = [
'title' => $this->t('Changed year + month'),
'help' => $this->t('Date in the form of YYYYMM.'),
'argument' => [
'field' => 'changed',
'id' => 'date_year_month',
],
];
$data['comment_field_data']['changed_year'] = [
'title' => $this->t('Changed year'),
'help' => $this->t('Date in the form of YYYY.'),
'argument' => [
'field' => 'changed',
'id' => 'date_year',
],
];
$data['comment_field_data']['changed_month'] = [
'title' => $this->t('Changed month'),
'help' => $this->t('Date in the form of MM (01 - 12).'),
'argument' => [
'field' => 'changed',
'id' => 'date_month',
],
];
$data['comment_field_data']['changed_day'] = [
'title' => $this->t('Changed day'),
'help' => $this->t('Date in the form of DD (01 - 31).'),
'argument' => [
'field' => 'changed',
'id' => 'date_day',
],
];
$data['comment_field_data']['changed_week'] = [
'title' => $this->t('Changed week'),
'help' => $this->t('Date in the form of WW (01 - 53).'),
'argument' => [
'field' => 'changed',
'id' => 'date_week',
],
];
$data['comment_field_data']['status']['title'] = $this->t('Approved status');
$data['comment_field_data']['status']['help'] = $this->t('Whether the comment is approved (or still in the moderation queue).');
$data['comment_field_data']['status']['filter']['label'] = $this->t('Approved comment status');
$data['comment_field_data']['status']['filter']['type'] = 'yes-no';
$data['comment']['approve_comment'] = [
'field' => [
'title' => $this->t('Link to approve comment'),
'help' => $this->t('Provide a simple link to approve the comment.'),
'id' => 'comment_link_approve',
],
];
$data['comment']['replyto_comment'] = [
'field' => [
'title' => $this->t('Link to reply-to comment'),
'help' => $this->t('Provide a simple link to reply to the comment.'),
'id' => 'comment_link_reply',
],
];
$data['comment_field_data']['entity_id']['field']['id'] = 'commented_entity';
unset($data['comment_field_data']['entity_id']['relationship']);
$data['comment']['comment_bulk_form'] = [
'title' => $this->t('Comment operations bulk form'),
'help' => $this->t('Add a form element that lets you run operations on multiple comments.'),
'field' => [
'id' => 'comment_bulk_form',
],
];
$data['comment_field_data']['thread']['field'] = [
'title' => $this->t('Depth'),
'help' => $this->t('Display the depth of the comment if it is threaded.'),
'id' => 'comment_depth',
];
$data['comment_field_data']['thread']['sort'] = [
'title' => $this->t('Thread'),
'help' => $this->t('Sort by the threaded order. This will keep child comments together with their parents.'),
'id' => 'comment_thread',
];
unset($data['comment_field_data']['thread']['filter']);
unset($data['comment_field_data']['thread']['argument']);
$entities_types = \Drupal::entityTypeManager()->getDefinitions();
// Provide a relationship for each entity type except comment.
foreach ($entities_types as $type => $entity_type) {
if ($type == 'comment' || !$entity_type->entityClassImplements(ContentEntityInterface::class) || !$entity_type->getBaseTable()) {
continue;
}
if (\Drupal::service('comment.manager')->getFields($type)) {
$data['comment_field_data'][$type] = [
'relationship' => [
'title' => $entity_type->getLabel(),
'help' => $this->t('The @entity_type to which the comment is a reply to.', ['@entity_type' => $entity_type->getLabel()]),
'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
'base field' => $entity_type->getKey('id'),
'relationship field' => 'entity_id',
'id' => 'standard',
'label' => $entity_type->getLabel(),
'extra' => [
[
'field' => 'entity_type',
'value' => $type,
'table' => 'comment_field_data',
],
],
],
];
}
}
$data['comment_field_data']['uid']['title'] = $this->t('Author uid');
$data['comment_field_data']['uid']['help'] = $this->t('If you need more fields than the uid add the comment: author relationship');
$data['comment_field_data']['uid']['relationship']['title'] = $this->t('Author');
$data['comment_field_data']['uid']['relationship']['help'] = $this->t("The User ID of the comment's author.");
$data['comment_field_data']['uid']['relationship']['label'] = $this->t('author');
$data['comment_field_data']['pid']['title'] = $this->t('Parent CID');
$data['comment_field_data']['pid']['relationship']['title'] = $this->t('Parent comment');
$data['comment_field_data']['pid']['relationship']['help'] = $this->t('The parent comment');
$data['comment_field_data']['pid']['relationship']['label'] = $this->t('parent');
// Define the base group of this table. Fields that don't have a group defined
// will go into this field by default.
$data['comment_entity_statistics']['table']['group'] = $this->t('Comment Statistics');
// Provide a relationship for each entity type except comment.
foreach ($entities_types as $type => $entity_type) {
if ($type == 'comment' || !$entity_type->entityClassImplements(ContentEntityInterface::class) || !$entity_type->getBaseTable()) {
continue;
}
// This relationship does not use the 'field id' column, if the entity has
// multiple comment-fields, then this might introduce duplicates, in which
// case the site-builder should enable aggregation and SUM the comment_count
// field. We cannot create a relationship from the base table to
// {comment_entity_statistics} for each field as multiple joins between
// the same two tables is not supported.
if (\Drupal::service('comment.manager')->getFields($type)) {
$data['comment_entity_statistics']['table']['join'][$entity_type->getDataTable() ?: $entity_type->getBaseTable()] = [
'type' => 'LEFT',
'left_field' => $entity_type->getKey('id'),
'field' => 'entity_id',
'extra' => [
[
'field' => 'entity_type',
'value' => $type,
],
],
];
}
}
$data['comment_entity_statistics']['last_comment_timestamp'] = [
'title' => $this->t('Last comment time'),
'help' => $this->t('Date and time of when the last comment was posted.'),
'field' => [
'id' => 'comment_last_timestamp',
],
'sort' => [
'id' => 'date',
],
'filter' => [
'id' => 'date',
],
];
$data['comment_entity_statistics']['last_comment_name'] = [
'title' => $this->t("Last comment author"),
'help' => $this->t('The name of the author of the last posted comment.'),
'field' => [
'id' => 'comment_ces_last_comment_name',
'no group by' => TRUE,
],
'sort' => [
'id' => 'comment_ces_last_comment_name',
'no group by' => TRUE,
],
];
$data['comment_entity_statistics']['comment_count'] = [
'title' => $this->t('Comment count'),
'help' => $this->t('The number of comments an entity has.'),
'field' => [
'id' => 'numeric',
],
'filter' => [
'id' => 'numeric',
],
'sort' => [
'id' => 'standard',
],
'argument' => [
'id' => 'standard',
],
];
$data['comment_entity_statistics']['last_updated'] = [
'title' => $this->t('Updated/commented date'),
'help' => $this->t('The most recent of last comment posted or entity updated time.'),
'field' => [
'id' => 'comment_ces_last_updated',
'no group by' => TRUE,
],
'sort' => [
'id' => 'comment_ces_last_updated',
'no group by' => TRUE,
],
'filter' => [
'id' => 'comment_ces_last_updated',
],
];
$data['comment_entity_statistics']['cid'] = [
'title' => $this->t('Last comment CID'),
'help' => $this->t('Display the last comment of an entity'),
'relationship' => [
'title' => $this->t('Last comment'),
'help' => $this->t('The last comment of an entity.'),
'group' => $this->t('Comment'),
'base' => 'comment',
'base field' => 'cid',
'id' => 'standard',
'label' => $this->t('Last Comment'),
],
];
$data['comment_entity_statistics']['last_comment_uid'] = [
'title' => $this->t('Last comment uid'),
'help' => $this->t('The User ID of the author of the last comment of an entity.'),
'relationship' => [
'title' => $this->t('Last comment author'),
'base' => 'users',
'base field' => 'uid',
'id' => 'standard',
'label' => $this->t('Last comment author'),
],
'filter' => [
'id' => 'numeric',
],
'argument' => [
'id' => 'numeric',
],
'field' => [
'id' => 'numeric',
],
];
$data['comment_entity_statistics']['entity_type'] = [
'title' => $this->t('Entity type'),
'help' => $this->t('The entity type to which the comment is a reply to.'),
'field' => [
'id' => 'standard',
],
'filter' => [
'id' => 'string',
],
'argument' => [
'id' => 'string',
],
'sort' => [
'id' => 'standard',
],
];
$data['comment_entity_statistics']['field_name'] = [
'title' => $this->t('Comment field name'),
'help' => $this->t('The field name from which the comment originated.'),
'field' => [
'id' => 'standard',
],
'filter' => [
'id' => 'string',
],
'argument' => [
'id' => 'string',
],
'sort' => [
'id' => 'standard',
],
];
return $data;
}
}

View File

@@ -0,0 +1,349 @@
<?php
namespace Drupal\comment\Controller;
use Drupal\comment\CommentInterface;
use Drupal\comment\CommentManagerInterface;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Controller for the comment entity.
*
* @see \Drupal\comment\Entity\Comment.
*/
class CommentController extends ControllerBase {
/**
* The HTTP kernel.
*
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
*/
protected $httpKernel;
/**
* The comment manager service.
*
* @var \Drupal\comment\CommentManagerInterface
*/
protected $commentManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* Constructs a CommentController object.
*
* @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
* HTTP kernel to handle requests.
* @param \Drupal\comment\CommentManagerInterface $comment_manager
* The comment manager service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager service.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository service.
*/
public function __construct(HttpKernelInterface $http_kernel, CommentManagerInterface $comment_manager, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, EntityRepositoryInterface $entity_repository) {
$this->httpKernel = $http_kernel;
$this->commentManager = $comment_manager;
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->entityRepository = $entity_repository;
}
/**
* Publishes the specified comment.
*
* @param \Drupal\comment\CommentInterface $comment
* A comment entity.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function commentApprove(CommentInterface $comment) {
$comment->setPublished();
$comment->save();
$this->messenger()->addStatus($this->t('Comment approved.'));
$permalink_uri = $comment->permalink();
$permalink_uri->setAbsolute();
return new RedirectResponse($permalink_uri->toString());
}
/**
* Redirects comment links to the correct page depending on comment settings.
*
* Since comments are paged there is no way to guarantee which page a comment
* appears on. Comment paging and threading settings may be changed at any
* time. With threaded comments, an individual comment may move between pages
* as comments can be added either before or after it in the overall
* discussion. Therefore we use a central routing function for comment links,
* which calculates the page number based on current comment settings and
* returns the full comment view with the pager set dynamically.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request of the page.
* @param \Drupal\comment\CommentInterface $comment
* A comment entity.
*
* @return \Symfony\Component\HttpFoundation\Response
* The comment listing set to the page on which the comment appears.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function commentPermalink(Request $request, CommentInterface $comment) {
if ($entity = $comment->getCommentedEntity()) {
// Check access permissions for the entity.
if (!$entity->access('view')) {
throw new AccessDeniedHttpException();
}
$field_definition = $this->entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle())[$comment->getFieldName()];
// Find the current display page for this comment.
$page = $this->entityTypeManager()->getStorage('comment')->getDisplayOrdinal($comment, $field_definition->getSetting('default_mode'), $field_definition->getSetting('per_page'));
// @todo Cleaner sub request handling.
$subrequest_url = $entity->toUrl()->setOption('query', ['page' => $page])->toString(TRUE);
$redirect_request = Request::create($subrequest_url->getGeneratedUrl(), 'GET', $request->query->all(), $request->cookies->all(), [], $request->server->all());
// Carry over the session to the subrequest.
$redirect_request->setSession($request->getSession());
$request->query->set('page', $page);
$response = $this->httpKernel->handle($redirect_request, HttpKernelInterface::SUB_REQUEST);
if ($response instanceof CacheableResponseInterface) {
// @todo Once path aliases have cache tags (see
// https://www.drupal.org/node/2480077), add test coverage that
// the cache tag for a commented entity's path alias is added to the
// comment's permalink response, because there can be blocks or
// other content whose renderings depend on the subrequest's URL.
$response->addCacheableDependency($subrequest_url);
}
return $response;
}
throw new NotFoundHttpException();
}
/**
* The _title_callback for the page that renders the comment permalink.
*
* @param \Drupal\comment\CommentInterface $comment
* The current comment.
*
* @return string
* The translated comment subject.
*/
public function commentPermalinkTitle(CommentInterface $comment) {
return $this->entityRepository->getTranslationFromContext($comment)->label();
}
/**
* Redirects legacy node links to the new path.
*
* @param \Drupal\Core\Entity\EntityInterface $node
* The node object identified by the legacy URL.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Redirects user to new URL.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function redirectNode(EntityInterface $node) {
$fields = $this->commentManager->getFields('node');
// Legacy nodes only had a single comment field, so use the first comment
// field on the entity.
if (!empty($fields) && ($field_names = array_keys($fields)) && ($field_name = reset($field_names))) {
return $this->redirect('comment.reply', [
'entity_type' => 'node',
'entity' => $node->id(),
'field_name' => $field_name,
]);
}
throw new NotFoundHttpException();
}
/**
* Form constructor for the comment reply form.
*
* There are several cases that have to be handled, including:
* - replies to comments
* - replies to entities
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity this comment belongs to.
* @param string $field_name
* The field_name to which the comment belongs.
* @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.
*
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
* An associative array containing:
* - An array for rendering the entity or parent comment.
* - comment_entity: If the comment is a reply to the entity.
* - comment_parent: If the comment is a reply to another comment.
* - comment_form: The comment form as a renderable array.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function getReplyForm(Request $request, EntityInterface $entity, $field_name, $pid = NULL) {
$account = $this->currentUser();
$build = [];
// The user is not just previewing a comment.
if ($request->request->get('op') != $this->t('Preview')) {
// $pid indicates that this is a reply to a comment.
if ($pid) {
// Load the parent comment.
$comment = $this->entityTypeManager()->getStorage('comment')->load($pid);
// Display the parent comment.
$build['comment_parent'] = $this->entityTypeManager()->getViewBuilder('comment')->view($comment);
}
// The comment is in response to an entity.
elseif ($entity->access('view', $account)) {
// We make sure the field value isn't set so we don't end up with a
// redirect loop.
$entity = clone $entity;
$entity->{$field_name}->status = CommentItemInterface::HIDDEN;
// Render array of the entity full view mode.
$build['commented_entity'] = $this->entityTypeManager()->getViewBuilder($entity->getEntityTypeId())->view($entity, 'full');
unset($build['commented_entity']['#cache']);
}
}
else {
$build['#title'] = $this->t('Preview comment');
}
// Show the actual reply box.
$comment = $this->entityTypeManager()->getStorage('comment')->create([
'entity_id' => $entity->id(),
'pid' => $pid,
'entity_type' => $entity->getEntityTypeId(),
'field_name' => $field_name,
]);
$build['comment_form'] = $this->entityFormBuilder()->getForm($comment);
return $build;
}
/**
* Access check for the reply form.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity this comment belongs to.
* @param string $field_name
* The field_name to which the comment belongs.
* @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.
*
* @return \Drupal\Core\Access\AccessResultInterface
* An access result
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function replyFormAccess(EntityInterface $entity, $field_name, $pid = NULL) {
// Check if entity and field exists.
$fields = $this->commentManager->getFields($entity->getEntityTypeId());
if (empty($fields[$field_name])) {
throw new NotFoundHttpException();
}
$account = $this->currentUser();
// Check if the user has the proper permissions.
$access = AccessResult::allowedIfHasPermission($account, 'post comments');
// If commenting is open on the entity.
$status = $entity->{$field_name}->status;
$access = $access->andIf(AccessResult::allowedIf($status == CommentItemInterface::OPEN)
->addCacheableDependency($entity))
// And if user has access to the host entity.
->andIf(AccessResult::allowedIf($entity->access('view')));
// $pid indicates that this is a reply to a comment.
if ($pid) {
// Check if the user has the proper permissions.
$access = $access->andIf(AccessResult::allowedIfHasPermission($account, 'access comments'));
// Load the parent comment.
$comment = $this->entityTypeManager()->getStorage('comment')->load($pid);
// Check if the parent comment is published and belongs to the entity.
$access = $access->andIf(AccessResult::allowedIf($comment && $comment->isPublished() && $comment->getCommentedEntityId() == $entity->id()));
if ($comment) {
$access->addCacheableDependency($comment);
}
}
return $access;
}
/**
* Returns a set of nodes' last read timestamps.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request of the page.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The JSON response.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function renderNewCommentsNodeLinks(Request $request) {
if ($this->currentUser()->isAnonymous()) {
throw new AccessDeniedHttpException();
}
if (!$request->request->has('node_ids') || !$request->request->has('field_name')) {
throw new NotFoundHttpException();
}
$nids = $request->request->all('node_ids');
$field_name = $request->request->get('field_name');
// Only handle up to 100 nodes.
$nids = array_slice($nids, 0, 100);
$links = [];
foreach ($nids as $nid) {
$node = $this->entityTypeManager()->getStorage('node')->load($nid);
$new = $this->commentManager->getCountNewComments($node);
$page_number = $this->entityTypeManager()->getStorage('comment')
->getNewCommentPageNumber($node->{$field_name}->comment_count, $new, $node, $field_name);
$query = $page_number ? ['page' => $page_number] : NULL;
$links[$nid] = [
'new_comment_count' => (int) $new,
'first_new_comment_link' => Url::fromRoute('entity.node.canonical', ['node' => $node->id()], ['query' => $query, 'fragment' => 'new'])->toString(),
];
}
return new JsonResponse($links);
}
}

View File

@@ -0,0 +1,559 @@
<?php
namespace Drupal\comment\Entity;
use Drupal\Component\Utility\Number;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\comment\CommentInterface;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityPublishedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\user\Entity\User;
use Drupal\user\EntityOwnerTrait;
/**
* Defines the comment entity class.
*
* @ContentEntityType(
* id = "comment",
* label = @Translation("Comment"),
* label_singular = @Translation("comment"),
* label_plural = @Translation("comments"),
* label_count = @PluralTranslation(
* singular = "@count comment",
* plural = "@count comments",
* ),
* bundle_label = @Translation("Comment type"),
* handlers = {
* "storage" = "Drupal\comment\CommentStorage",
* "storage_schema" = "Drupal\comment\CommentStorageSchema",
* "access" = "Drupal\comment\CommentAccessControlHandler",
* "list_builder" = "Drupal\Core\Entity\EntityListBuilder",
* "view_builder" = "Drupal\comment\CommentViewBuilder",
* "views_data" = "Drupal\comment\CommentViewsData",
* "form" = {
* "default" = "Drupal\comment\CommentForm",
* "delete" = "Drupal\comment\Form\DeleteForm"
* },
* "translation" = "Drupal\comment\CommentTranslationHandler"
* },
* base_table = "comment",
* data_table = "comment_field_data",
* uri_callback = "comment_uri",
* translatable = TRUE,
* entity_keys = {
* "id" = "cid",
* "bundle" = "comment_type",
* "label" = "subject",
* "langcode" = "langcode",
* "uuid" = "uuid",
* "published" = "status",
* "owner" = "uid",
* },
* links = {
* "canonical" = "/comment/{comment}",
* "delete-form" = "/comment/{comment}/delete",
* "delete-multiple-form" = "/admin/content/comment/delete",
* "edit-form" = "/comment/{comment}/edit",
* "create" = "/comment",
* },
* bundle_entity_type = "comment_type",
* field_ui_base_route = "entity.comment_type.edit_form",
* constraints = {
* "CommentName" = {}
* }
* )
*/
class Comment extends ContentEntityBase implements CommentInterface {
use EntityChangedTrait;
use EntityOwnerTrait;
use EntityPublishedTrait;
/**
* The thread for which a lock was acquired.
*
* @var string
*/
protected $threadLock = '';
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
if ($this->isNew()) {
// Add the comment to database. This next section builds the thread field.
// @see \Drupal\comment\CommentViewBuilder::buildComponents()
$thread = $this->getThread();
if (empty($thread)) {
if ($this->threadLock) {
// Thread lock was not released after being set previously.
// This suggests there's a bug in code using this class.
throw new \LogicException('preSave() is called again without calling postSave() or releaseThreadLock()');
}
if (!$this->hasParentComment()) {
// This is a comment with no parent comment (depth 0): we start
// by retrieving the maximum thread level.
$max = $storage->getMaxThread($this);
// Strip the "/" from the end of the thread.
$max = rtrim((string) $max, '/');
// We need to get the value at the correct depth.
$parts = explode('.', $max);
$n = Number::alphadecimalToInt($parts[0]);
$prefix = '';
}
else {
// This is a comment with a parent comment, so increase the part of
// the thread value at the proper depth.
// Get the parent comment:
$parent = $this->getParentComment();
// Strip the "/" from the end of the parent thread.
$parent->setThread((string) rtrim((string) $parent->getThread(), '/'));
$prefix = $parent->getThread() . '.';
// Get the max value in *this* thread.
$max = $storage->getMaxThreadPerThread($this);
if ($max == '') {
// First child of this parent. As the other two cases do an
// increment of the thread number before creating the thread
// string set this to -1 so it requires an increment too.
$n = -1;
}
else {
// Strip the "/" at the end of the thread.
$max = rtrim($max, '/');
// Get the value at the correct depth.
$parts = explode('.', $max);
$parent_depth = count(explode('.', $parent->getThread()));
$n = Number::alphadecimalToInt($parts[$parent_depth]);
}
}
// Finally, build the thread field for this new comment. To avoid
// race conditions, get a lock on the thread. If another process already
// has the lock, just move to the next integer.
do {
$thread = $prefix . Number::intToAlphadecimal(++$n) . '/';
$lock_name = "comment:{$this->getCommentedEntityId()}:$thread";
} while (!\Drupal::lock()->acquire($lock_name));
$this->threadLock = $lock_name;
}
$this->setThread($thread);
}
// The entity fields for name and mail have no meaning if the user is not
// Anonymous. Set them to NULL to make it clearer that they are not used.
// For anonymous users see \Drupal\comment\CommentForm::form() for mail,
// and \Drupal\comment\CommentForm::buildEntity() for name setting.
if (!$this->getOwner()->isAnonymous()) {
$this->set('name', NULL);
$this->set('mail', NULL);
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
// Always invalidate the cache tag for the commented entity.
if ($commented_entity = $this->getCommentedEntity()) {
Cache::invalidateTags($commented_entity->getCacheTagsToInvalidate());
}
$this->releaseThreadLock();
// Update the {comment_entity_statistics} table prior to executing the hook.
\Drupal::service('comment.statistics')->update($this);
}
/**
* Release the lock acquired for the thread in preSave().
*/
protected function releaseThreadLock() {
if ($this->threadLock) {
\Drupal::lock()->release($this->threadLock);
$this->threadLock = '';
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
$child_cids = $storage->getChildCids($entities);
$comment_storage = \Drupal::entityTypeManager()->getStorage('comment');
$comments = $comment_storage->loadMultiple($child_cids);
$comment_storage->delete($comments);
foreach ($entities as $entity) {
\Drupal::service('comment.statistics')->update($entity);
}
}
/**
* {@inheritdoc}
*/
public function referencedEntities() {
$referenced_entities = parent::referencedEntities();
if ($this->getCommentedEntityId()) {
$referenced_entities[] = $this->getCommentedEntity();
}
return $referenced_entities;
}
/**
* {@inheritdoc}
*/
public function permalink() {
$uri = $this->toUrl();
$uri->setOption('fragment', 'comment-' . $this->id());
return $uri;
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
$fields = parent::baseFieldDefinitions($entity_type);
$fields += static::publishedBaseFieldDefinitions($entity_type);
$fields += static::ownerBaseFieldDefinitions($entity_type);
$fields['cid']->setLabel(t('Comment ID'))
->setDescription(t('The comment ID.'));
$fields['uuid']->setDescription(t('The comment UUID.'));
$fields['comment_type']->setLabel(t('Comment Type'))
->setDescription(t('The comment type.'));
$fields['langcode']->setDescription(t('The comment language code.'));
// Set the default value callback for the status field.
$fields['status']->setDefaultValueCallback('Drupal\comment\Entity\Comment::getDefaultStatus');
$fields['pid'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Parent ID'))
->setDescription(t('The parent comment ID if this is a reply to a comment.'))
->setSetting('target_type', 'comment');
$fields['entity_id'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Entity ID'))
->setDescription(t('The ID of the entity of which this comment is a reply.'))
->setRequired(TRUE);
$fields['subject'] = BaseFieldDefinition::create('string')
->setLabel(t('Subject'))
->setTranslatable(TRUE)
->setSetting('max_length', 64)
->setDisplayOptions('form', [
'type' => 'string_textfield',
// Default comment body field has weight 20.
'weight' => 10,
])
->setDisplayConfigurable('form', TRUE);
$fields['uid']
->setDescription(t('The user ID of the comment author.'));
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setDescription(t("The comment author's name."))
->setTranslatable(TRUE)
->setSetting('max_length', 60)
->setDefaultValue('');
$fields['mail'] = BaseFieldDefinition::create('email')
->setLabel(t('Email'))
->setDescription(t("The comment author's email address."))
->setTranslatable(TRUE);
$fields['homepage'] = BaseFieldDefinition::create('uri')
->setLabel(t('Homepage'))
->setDescription(t("The comment author's home page address."))
->setTranslatable(TRUE)
// URIs are not length limited by RFC 2616, but we can only store 255
// characters in our comment DB schema.
->setSetting('max_length', 255);
$fields['hostname'] = BaseFieldDefinition::create('string')
->setLabel(t('Hostname'))
->setDescription(t("The comment author's hostname."))
->setTranslatable(TRUE)
->setSetting('max_length', 128)
->setDefaultValueCallback(static::class . '::getDefaultHostname');
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Created'))
->setDescription(t('The time that the comment was created.'))
->setTranslatable(TRUE);
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the comment was last edited.'))
->setTranslatable(TRUE);
$fields['thread'] = BaseFieldDefinition::create('string')
->setLabel(t('Thread place'))
->setDescription(t("The alphadecimal representation of the comment's place in a thread, consisting of a base 36 string prefixed by an integer indicating its length."))
->setSetting('max_length', 255);
$fields['entity_type'] = BaseFieldDefinition::create('string')
->setLabel(t('Entity type'))
->setRequired(TRUE)
->setDescription(t('The entity type to which this comment is attached.'))
->setSetting('is_ascii', TRUE)
->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH);
$fields['field_name'] = BaseFieldDefinition::create('string')
->setLabel(t('Comment field name'))
->setRequired(TRUE)
->setDescription(t('The field name through which this comment was added.'))
->setSetting('is_ascii', TRUE)
->setSetting('max_length', FieldStorageConfig::NAME_MAX_LENGTH);
return $fields;
}
/**
* {@inheritdoc}
*/
public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
if ($comment_type = CommentType::load($bundle)) {
$fields['entity_id'] = clone $base_field_definitions['entity_id'];
$fields['entity_id']->setSetting('target_type', $comment_type->getTargetEntityTypeId());
return $fields;
}
return [];
}
/**
* {@inheritdoc}
*/
public function hasParentComment() {
return (bool) $this->get('pid')->target_id;
}
/**
* {@inheritdoc}
*/
public function getParentComment() {
return $this->get('pid')->entity;
}
/**
* {@inheritdoc}
*/
public function getCommentedEntity() {
return $this->get('entity_id')->entity;
}
/**
* {@inheritdoc}
*/
public function getCommentedEntityId() {
return $this->get('entity_id')->target_id;
}
/**
* {@inheritdoc}
*/
public function getCommentedEntityTypeId() {
return $this->get('entity_type')->value;
}
/**
* {@inheritdoc}
*/
public function setFieldName($field_name) {
$this->set('field_name', $field_name);
return $this;
}
/**
* {@inheritdoc}
*/
public function getFieldName() {
return $this->get('field_name')->value;
}
/**
* {@inheritdoc}
*/
public function getSubject() {
return $this->get('subject')->value ?? '';
}
/**
* {@inheritdoc}
*/
public function setSubject($subject) {
$this->set('subject', $subject);
return $this;
}
/**
* {@inheritdoc}
*/
public function getAuthorName() {
// If their is a valid user id and the user entity exists return the label.
if ($this->get('uid')->target_id && $this->get('uid')->entity) {
return $this->get('uid')->entity->label();
}
return $this->get('name')->value ?: \Drupal::config('user.settings')->get('anonymous');
}
/**
* {@inheritdoc}
*/
public function setAuthorName($name) {
$this->set('name', $name);
return $this;
}
/**
* {@inheritdoc}
*/
public function getAuthorEmail() {
$mail = $this->get('mail')->value;
if ($this->get('uid')->target_id != 0) {
$mail = $this->get('uid')->entity->getEmail();
}
return $mail;
}
/**
* {@inheritdoc}
*/
public function getHomepage() {
return $this->get('homepage')->value;
}
/**
* {@inheritdoc}
*/
public function setHomepage($homepage) {
$this->set('homepage', $homepage);
return $this;
}
/**
* {@inheritdoc}
*/
public function getHostname() {
return $this->get('hostname')->value;
}
/**
* {@inheritdoc}
*/
public function setHostname($hostname) {
$this->set('hostname', $hostname);
return $this;
}
/**
* {@inheritdoc}
*/
public function getCreatedTime() {
if (isset($this->get('created')->value)) {
return $this->get('created')->value;
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function setCreatedTime($created) {
$this->set('created', $created);
return $this;
}
/**
* {@inheritdoc}
*/
public function getThread() {
$thread = $this->get('thread');
if (!empty($thread->value)) {
return $thread->value;
}
}
/**
* {@inheritdoc}
*/
public function setThread($thread) {
$this->set('thread', $thread);
return $this;
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageInterface $storage, array &$values) {
if (empty($values['comment_type']) && !empty($values['field_name']) && !empty($values['entity_type'])) {
$fields = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($values['entity_type']);
$values['comment_type'] = $fields[$values['field_name']]->getSetting('comment_type');
}
}
/**
* {@inheritdoc}
*/
public function getOwner() {
$user = $this->get('uid')->entity;
if (!$user || $user->isAnonymous()) {
$user = User::getAnonymousUser();
$user->name = $this->getAuthorName();
$user->homepage = $this->getHomepage();
}
return $user;
}
/**
* Get the comment type ID for this comment.
*
* @return string
* The ID of the comment type.
*/
public function getTypeId() {
return $this->bundle();
}
/**
* Default value callback for 'status' base field definition.
*
* @see ::baseFieldDefinitions()
*
* @return bool
* TRUE if the comment should be published, FALSE otherwise.
*/
public static function getDefaultStatus() {
return \Drupal::currentUser()->hasPermission('skip comment approval') ? CommentInterface::PUBLISHED : CommentInterface::NOT_PUBLISHED;
}
/**
* Returns the default value for entity hostname base field.
*
* @return string
* The client host name.
*/
public static function getDefaultHostname() {
if (\Drupal::config('comment.settings')->get('log_ip_addresses')) {
return \Drupal::request()->getClientIP();
}
return '';
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Drupal\comment\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
use Drupal\comment\CommentTypeInterface;
/**
* Defines the comment type entity.
*
* @ConfigEntityType(
* id = "comment_type",
* label = @Translation("Comment type"),
* label_singular = @Translation("comment type"),
* label_plural = @Translation("comment types"),
* label_count = @PluralTranslation(
* singular = "@count comment type",
* plural = "@count comment types",
* ),
* handlers = {
* "form" = {
* "default" = "Drupal\comment\CommentTypeForm",
* "add" = "Drupal\comment\CommentTypeForm",
* "edit" = "Drupal\comment\CommentTypeForm",
* "delete" = "Drupal\comment\Form\CommentTypeDeleteForm"
* },
* "route_provider" = {
* "permissions" = "Drupal\user\Entity\EntityPermissionsRouteProviderWithCheck",
* },
* "list_builder" = "Drupal\comment\CommentTypeListBuilder"
* },
* admin_permission = "administer comment types",
* config_prefix = "type",
* bundle_of = "comment",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* links = {
* "delete-form" = "/admin/structure/comment/manage/{comment_type}/delete",
* "edit-form" = "/admin/structure/comment/manage/{comment_type}",
* "add-form" = "/admin/structure/comment/types/add",
* "entity-permissions-form" = "/admin/structure/comment/manage/{comment_type}/permissions",
* "collection" = "/admin/structure/comment",
* },
* config_export = {
* "id",
* "label",
* "target_entity_type_id",
* "description",
* }
* )
*/
class CommentType extends ConfigEntityBundleBase implements CommentTypeInterface {
/**
* The comment type ID.
*
* @var string
*/
protected $id;
/**
* The comment type label.
*
* @var string
*/
protected $label;
/**
* The description of the comment type.
*
* @var string
*/
protected $description;
/**
* The target entity type.
*
* @var string
*/
protected $target_entity_type_id;
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description;
}
/**
* {@inheritdoc}
*/
public function setDescription($description) {
$this->description = $description;
return $this;
}
/**
* {@inheritdoc}
*/
public function getTargetEntityTypeId() {
return $this->target_entity_type_id;
}
}

View File

@@ -0,0 +1,300 @@
<?php
namespace Drupal\comment\Form;
use Drupal\comment\CommentInterface;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the comments overview administration form.
*
* @internal
*/
class CommentAdminOverview extends FormBase {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The comment storage.
*
* @var \Drupal\comment\CommentStorageInterface
*/
protected $commentStorage;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The tempstore factory.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempStoreFactory;
/**
* Creates a CommentAdminOverview form.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, DateFormatterInterface $date_formatter, ModuleHandlerInterface $module_handler, PrivateTempStoreFactory $temp_store_factory) {
$this->entityTypeManager = $entity_type_manager;
$this->commentStorage = $entity_type_manager->getStorage('comment');
$this->dateFormatter = $date_formatter;
$this->moduleHandler = $module_handler;
$this->tempStoreFactory = $temp_store_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('date.formatter'),
$container->get('module_handler'),
$container->get('tempstore.private')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'comment_admin_overview';
}
/**
* Form constructor for the comment overview administration form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param string $type
* The type of the overview form ('approval' or 'new').
*
* @return array
* The form structure.
*/
public function buildForm(array $form, FormStateInterface $form_state, $type = 'new') {
// Build an 'Update options' form.
$form['options'] = [
'#type' => 'details',
'#title' => $this->t('Update options'),
'#open' => TRUE,
'#attributes' => ['class' => ['container-inline']],
];
if ($type == 'approval') {
$options['publish'] = $this->t('Publish the selected comments');
}
else {
$options['unpublish'] = $this->t('Unpublish the selected comments');
}
$options['delete'] = $this->t('Delete the selected comments');
$form['options']['operation'] = [
'#type' => 'select',
'#title' => $this->t('Action'),
'#title_display' => 'invisible',
'#options' => $options,
'#default_value' => 'publish',
];
$form['options']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Update'),
];
// Load the comments that need to be displayed.
$status = ($type == 'approval') ? CommentInterface::NOT_PUBLISHED : CommentInterface::PUBLISHED;
$header = [
'subject' => [
'data' => $this->t('Subject'),
'specifier' => 'subject',
],
'author' => [
'data' => $this->t('Author'),
'specifier' => 'name',
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
],
'posted_in' => [
'data' => $this->t('Posted in'),
'class' => [RESPONSIVE_PRIORITY_LOW],
],
'changed' => [
'data' => $this->t('Updated'),
'specifier' => 'changed',
'sort' => 'desc',
'class' => [RESPONSIVE_PRIORITY_LOW],
],
'operations' => $this->t('Operations'),
];
$cids = $this->commentStorage->getQuery()
->accessCheck(TRUE)
->condition('status', $status)
->tableSort($header)
->pager(50)
->execute();
/** @var \Drupal\comment\CommentInterface[] $comments */
$comments = $this->commentStorage->loadMultiple($cids);
// Build a table listing the appropriate comments.
$options = [];
$destination = $this->getDestinationArray();
$commented_entity_ids = [];
$commented_entities = [];
foreach ($comments as $comment) {
$commented_entity_ids[$comment->getCommentedEntityTypeId()][] = $comment->getCommentedEntityId();
}
foreach ($commented_entity_ids as $entity_type => $ids) {
$commented_entities[$entity_type] = $this->entityTypeManager
->getStorage($entity_type)
->loadMultiple($ids);
}
foreach ($comments as $comment) {
/** @var \Drupal\Core\Entity\EntityInterface $commented_entity */
$commented_entity = $commented_entities[$comment->getCommentedEntityTypeId()][$comment->getCommentedEntityId()];
$comment_permalink = $comment->permalink();
if ($comment->hasField('comment_body') && ($body = $comment->get('comment_body')->value)) {
$attributes = $comment_permalink->getOption('attributes') ?: [];
$attributes += ['title' => Unicode::truncate($body, 128)];
$comment_permalink->setOption('attributes', $attributes);
}
$options[$comment->id()] = [
'title' => ['data' => ['#title' => $comment->getSubject() ?: $comment->id()]],
'subject' => [
'data' => [
'#type' => 'link',
'#title' => $comment->getSubject(),
'#url' => $comment_permalink,
],
],
'author' => [
'data' => [
'#theme' => 'username',
'#account' => $comment->getOwner(),
],
],
'posted_in' => [
'data' => [
'#type' => 'link',
'#title' => $commented_entity->label(),
'#access' => $commented_entity->access('view'),
'#url' => $commented_entity->toUrl(),
],
],
'changed' => $this->dateFormatter->format($comment->getChangedTimeAcrossTranslations(), 'short'),
];
$comment_uri_options = $comment->toUrl()->getOptions() + ['query' => $destination];
$links = [];
$links['edit'] = [
'title' => $this->t('Edit'),
'url' => $comment->toUrl('edit-form', $comment_uri_options),
];
if ($this->moduleHandler->moduleExists('content_translation') && $this->moduleHandler->invoke('content_translation', 'translate_access', [$comment])->isAllowed()) {
$links['translate'] = [
'title' => $this->t('Translate'),
'url' => $comment->toUrl('drupal:content-translation-overview', $comment_uri_options),
];
}
$options[$comment->id()]['operations']['data'] = [
'#type' => 'operations',
'#links' => $links,
];
}
$form['comments'] = [
'#type' => 'tableselect',
'#header' => $header,
'#options' => $options,
'#empty' => $this->t('No comments available.'),
];
$form['pager'] = ['#type' => 'pager'];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$form_state->setValue('comments', array_diff($form_state->getValue('comments'), [0]));
// We can't execute any 'Update options' if no comments were selected.
if (count($form_state->getValue('comments')) == 0) {
$form_state->setErrorByName('', $this->t('Select one or more comments to perform the update on.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$operation = $form_state->getValue('operation');
$cids = $form_state->getValue('comments');
/** @var \Drupal\comment\CommentInterface[] $comments */
$comments = $this->commentStorage->loadMultiple($cids);
if ($operation != 'delete') {
foreach ($comments as $comment) {
if ($operation == 'unpublish') {
$comment->setUnpublished();
}
elseif ($operation == 'publish') {
$comment->setPublished();
}
$comment->save();
}
$this->messenger()->addStatus($this->t('The update has been performed.'));
$form_state->setRedirect('comment.admin');
}
else {
$info = [];
/** @var \Drupal\comment\CommentInterface $comment */
foreach ($comments as $comment) {
$langcode = $comment->language()->getId();
$info[$comment->id()][$langcode] = $langcode;
}
$this->tempStoreFactory
->get('entity_delete_multiple_confirm')
->set($this->currentUser()->id() . ':comment', $info);
$form_state->setRedirect('entity.comment.delete_multiple_form');
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Drupal\comment\Form;
use Drupal\comment\CommentManagerInterface;
use Drupal\Core\Entity\EntityDeleteForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a confirmation form for deleting a comment type entity.
*
* @internal
*/
class CommentTypeDeleteForm extends EntityDeleteForm {
/**
* The comment manager service.
*
* @var \Drupal\comment\CommentManagerInterface
*/
protected $commentManager;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The entity being used by this form.
*
* @var \Drupal\comment\CommentTypeInterface
*/
protected $entity;
/**
* Constructs a query factory object.
*
* @param \Drupal\comment\CommentManagerInterface $comment_manager
* The comment manager service.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
*/
public function __construct(CommentManagerInterface $comment_manager, LoggerInterface $logger) {
$this->commentManager = $comment_manager;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('comment.manager'),
$container->get('logger.factory')->get('comment')
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$comment_count = $this->entityTypeManager->getStorage('comment')->getQuery()
->accessCheck(FALSE)
->condition('comment_type', $this->entity->id())
->count()
->execute();
$entity_type = $this->entity->getTargetEntityTypeId();
$caption = '';
foreach (array_keys($this->commentManager->getFields($entity_type)) as $field_name) {
/** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
if (($field_storage = FieldStorageConfig::loadByName($entity_type, $field_name)) && $field_storage->getSetting('comment_type') == $this->entity->id() && !$field_storage->isDeleted()) {
$caption .= '<p>' . $this->t('%label is used by the %field field on your site. You can not remove this comment type until you have removed the field.', [
'%label' => $this->entity->label(),
'%field' => $field_storage->label(),
]) . '</p>';
}
}
if ($comment_count) {
$caption .= '<p>' . $this->formatPlural($comment_count, '%label is used by 1 comment on your site. You can not remove this comment type until you have removed all of the %label comments.', '%label is used by @count comments on your site. You may not remove %label until you have removed all of the %label comments.', ['%label' => $this->entity->label()]) . '</p>';
}
if ($caption) {
$form['description'] = ['#markup' => $caption];
return $form;
}
else {
return parent::buildForm($form, $form_state);
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\comment\Form;
use Drupal\Core\Entity\Form\DeleteMultipleForm as EntityDeleteMultipleForm;
use Drupal\Core\Url;
/**
* Provides the comment multiple delete confirmation form.
*
* @internal
*/
class ConfirmDeleteMultiple extends EntityDeleteMultipleForm {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->formatPlural(count($this->selection), 'Are you sure you want to delete this comment and all its children?', 'Are you sure you want to delete these comments and all their children?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('comment.admin');
}
/**
* {@inheritdoc}
*/
protected function getDeletedMessage($count) {
return $this->formatPlural($count, 'Deleted @count comment.', 'Deleted @count comments.');
}
/**
* {@inheritdoc}
*/
protected function getInaccessibleMessage($count) {
return $this->formatPlural($count, "@count comment has not been deleted because you do not have the necessary permissions.", "@count comments have not been deleted because you do not have the necessary permissions.");
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Drupal\comment\Form;
use Drupal\Core\Entity\ContentEntityDeleteForm;
/**
* Provides the comment delete confirmation form.
*
* @internal
*/
class DeleteForm extends ContentEntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
// Point to the entity of which this comment is a reply.
return $this->entity->get('entity_id')->entity->toUrl();
}
/**
* {@inheritdoc}
*/
protected function getRedirectUrl() {
return $this->getCancelUrl();
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Any replies to this comment will be lost. This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
protected function getDeletionMessage() {
return $this->t('The comment and all its replies have been deleted.');
}
/**
* {@inheritdoc}
*/
public function logDeletionMessage() {
$this->logger('comment')->info('Deleted comment @cid and its replies.', ['@cid' => $this->entity->id()]);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Drupal\comment\Plugin\Action;
use Drupal\action\Plugin\Action\UnpublishByKeywordComment as ActionUnpublishByKeywordComment;
use Drupal\Core\Entity\EntityViewBuilderInterface;
use Drupal\Core\Render\RendererInterface;
/**
* Unpublishes a comment containing certain keywords.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
* \Drupal\action\Plugin\Action\UnpublishByKeywordComment instead.
*
* @see https://www.drupal.org/node/3424506
*/
class UnpublishByKeywordComment extends ActionUnpublishByKeywordComment {
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityViewBuilderInterface $comment_view_builder, RendererInterface $renderer) {
@trigger_error(__CLASS__ . ' is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\action\Plugin\Action\UnpublishByKeywordComment instead. See https://www.drupal.org/node/3424506', E_USER_DEPRECATED);
parent::__construct($configuration, $plugin_id, $plugin_definition, $comment_view_builder, $renderer);
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace Drupal\comment\Plugin\EntityReferenceSelection;
use Drupal\Component\Utility\Html;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\Attribute\EntityReferenceSelection;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\comment\CommentInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides specific access control for the comment entity type.
*/
#[EntityReferenceSelection(
id: "default:comment",
label: new TranslatableMarkup("Comment selection"),
entity_types: ["comment"],
group: "default",
weight: 1
)]
class CommentSelection extends DefaultSelection {
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
// Adding the 'comment_access' tag is sadly insufficient for comments:
// core requires us to also know about the concept of 'published' and
// 'unpublished'.
if (!$this->currentUser->hasPermission('administer comments')) {
$query->condition('status', CommentInterface::PUBLISHED);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$comment = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
// In order to create a referenceable comment, it needs to published.
/** @var \Drupal\comment\CommentInterface $comment */
$comment->setPublished();
return $comment;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableNewEntities(array $entities) {
$entities = parent::validateReferenceableNewEntities($entities);
// Mirror the conditions checked in buildEntityQuery().
if (!$this->currentUser->hasPermission('administer comments')) {
$entities = array_filter($entities, function ($comment) {
/** @var \Drupal\comment\CommentInterface $comment */
return $comment->isPublished();
});
}
return $entities;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableEntities(array $ids) {
$result = [];
if ($ids) {
$target_type = $this->configuration['target_type'];
$entity_type = $this->entityTypeManager->getDefinition($target_type);
$query = $this->buildEntityQuery();
// Mirror the conditions checked in buildEntityQuery().
if (!$this->currentUser->hasPermission('administer comments')) {
$query->condition('status', 1);
}
$result = $query
->condition($entity_type->getKey('id'), $ids, 'IN')
->execute();
}
return $result;
}
/**
* {@inheritdoc}
*/
public function entityQueryAlter(SelectInterface $query) {
parent::entityQueryAlter($query);
$tables = $query->getTables();
$data_table = 'comment_field_data';
if (!isset($tables['comment_field_data']['alias'])) {
// If no conditions join against the comment data table, it should be
// joined manually to allow node access processing.
$query->innerJoin($data_table, NULL, "[base_table].[cid] = [$data_table].[cid] AND [$data_table].[default_langcode] = 1");
}
// Historically, comments were always linked to 'node' entities, but that is
// no longer the case, as the 'node' module might not even be enabled.
// Comments can now be linked to any entity and they can also be referenced
// by other entities, so we won't have a single table to join to. That
// actually means that we can no longer optimize the query on those cases.
// However, the most common case remains to be comment replies, and in this
// case, we can get the host entity type if the 'entity' value is present
// and perform the extra joins and alterations needed.
$comment = $this->getConfiguration()['entity'];
if ($comment instanceof CommentInterface) {
$host_entity_type_id = $comment->getCommentedEntityTypeId();
/** @var \Drupal\Core\Entity\EntityTypeInterface $host_entity_type */
$host_entity_type = $this->entityTypeManager->getDefinition($host_entity_type_id);
$host_entity_field_data_table = $host_entity_type->getDataTable();
// Not all entities have a data table, so check first.
if ($host_entity_field_data_table) {
$id_key = $host_entity_type->getKey('id');
// The Comment module doesn't implement per-comment access, so it
// checks instead that the user has access to the host entity.
$entity_alias = $query->innerJoin($host_entity_field_data_table, 'n', "[%alias].[$id_key] = [$data_table].[entity_id] AND [$data_table].[entity_type] = '$host_entity_type_id'");
// Pass the query to the entity access control.
$this->reAlterQuery($query, $host_entity_type_id . '_access', $entity_alias);
// Additional checks for "node" entities.
if ($host_entity_type_id === 'node') {
// Passing the query to node_query_node_access_alter() is sadly
// insufficient for nodes.
// @see \Drupal\node\Plugin\EntityReferenceSelection\NodeSelection::buildEntityQuery()
if (!$this->currentUser->hasPermission('bypass node access') && !$this->moduleHandler->hasImplementations('node_grants')) {
$query->condition($entity_alias . '.status', 1);
}
}
}
}
}
/**
* {@inheritdoc}
*/
public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
$target_type = $this->getConfiguration()['target_type'];
$query = $this->buildEntityQuery($match, $match_operator);
if ($limit > 0) {
$query->range(0, $limit);
}
$result = $query->execute();
if (empty($result)) {
return [];
}
$options = [];
$entities = $this->entityTypeManager->getStorage($target_type)->loadMultiple($result);
foreach ($entities as $entity_id => $entity) {
// Additional access check as comments might be attached to entities
// which the current user does not have access to.
if ($entity->access('view', $this->currentUser)) {
$bundle = $entity->bundle();
$options[$bundle][$entity_id] = Html::escape($this->entityRepository->getTranslationFromContext($entity)->label() ?? '');
}
}
return $options;
}
/**
* {@inheritdoc}
*/
public function countReferenceableEntities($match = NULL, $match_operator = 'CONTAINS') {
$options = $this->getReferenceableEntities($match, $match_operator);
return count($options, COUNT_RECURSIVE) - count($options);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\comment\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'comment_username' formatter.
*/
#[FieldFormatter(
id: 'comment_username',
label: new TranslatableMarkup('Author name'),
description: new TranslatableMarkup('Display the author name.'),
field_types: [
'string',
],
)]
class AuthorNameFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($items as $delta => $item) {
/** @var \Drupal\comment\CommentInterface $comment */
$comment = $item->getEntity();
$account = $comment->getOwner();
$elements[$delta] = [
'#theme' => 'username',
'#account' => $account,
'#cache' => [
'tags' => $account->getCacheTags() + $comment->getCacheTags(),
],
];
}
return $elements;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return $field_definition->getName() === 'name' && $field_definition->getTargetEntityTypeId() === 'comment';
}
}

View File

@@ -0,0 +1,286 @@
<?php
namespace Drupal\comment\Plugin\Field\FieldFormatter;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityFormBuilderInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a default comment formatter.
*/
#[FieldFormatter(
id: 'comment_default',
label: new TranslatableMarkup('Comment list'),
field_types: [
'comment',
],
)]
class CommentDefaultFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'view_mode' => 'default',
'pager_id' => 0,
] + parent::defaultSettings();
}
/**
* The comment storage.
*
* @var \Drupal\comment\CommentStorageInterface
*/
protected $storage;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The comment render controller.
*
* @var \Drupal\Core\Entity\EntityViewBuilderInterface
*/
protected $viewBuilder;
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* The entity form builder.
*
* @var \Drupal\Core\Entity\EntityFormBuilderInterface
*/
protected $entityFormBuilder;
/**
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('current_user'),
$container->get('entity_type.manager'),
$container->get('entity.form_builder'),
$container->get('current_route_match'),
$container->get('entity_display.repository')
);
}
/**
* Constructs a new CommentDefaultFormatter.
*
* @param string $plugin_id
* The plugin_id for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the formatter is associated.
* @param array $settings
* The formatter settings.
* @param string $label
* The formatter label display setting.
* @param string $view_mode
* The view mode.
* @param array $third_party_settings
* Third party settings.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder
* The entity form builder.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, EntityFormBuilderInterface $entity_form_builder, RouteMatchInterface $route_match, EntityDisplayRepositoryInterface $entity_display_repository) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->viewBuilder = $entity_type_manager->getViewBuilder('comment');
$this->storage = $entity_type_manager->getStorage('comment');
$this->currentUser = $current_user;
$this->entityFormBuilder = $entity_form_builder;
$this->routeMatch = $route_match;
$this->entityDisplayRepository = $entity_display_repository;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
$output = [];
$field_name = $this->fieldDefinition->getName();
$entity = $items->getEntity();
$status = $items->status;
if ($status != CommentItemInterface::HIDDEN && empty($entity->in_preview) &&
// Comments are added to the search results and search index by
// comment_node_update_index() instead of by this formatter, so don't
// return anything if the view mode is search_index or search_result.
!in_array($this->viewMode, ['search_result', 'search_index'])) {
$comment_settings = $this->getFieldSettings();
// Only attempt to render comments if the entity has visible comments.
// Unpublished comments are not included in
// $entity->get($field_name)->comment_count, but unpublished comments
// should display if the user is an administrator.
$elements['#cache']['contexts'][] = 'user.permissions';
if ($this->currentUser->hasPermission('access comments') || $this->currentUser->hasPermission('administer comments')) {
$output['comments'] = [];
if ($entity->get($field_name)->comment_count || $this->currentUser->hasPermission('administer comments')) {
$mode = $comment_settings['default_mode'];
$comments_per_page = $comment_settings['per_page'];
$comments = $this->storage->loadThread($entity, $field_name, $mode, $comments_per_page, $this->getSetting('pager_id'));
if ($comments) {
$build = $this->viewBuilder->viewMultiple($comments, $this->getSetting('view_mode'));
$build['pager']['#type'] = 'pager';
// CommentController::commentPermalink() calculates the page number
// where a specific comment appears and does a subrequest pointing to
// that page, we need to pass that subrequest route to our pager to
// keep the pager working.
$build['pager']['#route_name'] = $this->routeMatch->getRouteName();
$build['pager']['#route_parameters'] = $this->routeMatch->getRawParameters()->all();
if ($this->getSetting('pager_id')) {
$build['pager']['#element'] = $this->getSetting('pager_id');
}
$output['comments'] += $build;
}
}
}
// Append comment form if the comments are open and the form is set to
// display below the entity. Do not show the form for the print view mode.
if ($status == CommentItemInterface::OPEN && $comment_settings['form_location'] == CommentItemInterface::FORM_BELOW && $this->viewMode != 'print') {
// Only show the add comment form if the user has permission.
$elements['#cache']['contexts'][] = 'user.roles';
if ($this->currentUser->hasPermission('post comments')) {
$output['comment_form'] = [
'#lazy_builder' => [
'comment.lazy_builders:renderForm',
[
$entity->getEntityTypeId(),
$entity->id(),
$field_name,
$this->getFieldSetting('comment_type'),
],
],
'#create_placeholder' => TRUE,
];
}
}
$elements[] = $output + [
'#comment_type' => $this->getFieldSetting('comment_type'),
'#comment_display_mode' => $this->getFieldSetting('default_mode'),
'comments' => [],
'comment_form' => [],
];
}
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element = [];
$view_modes = $this->getViewModes();
$element['view_mode'] = [
'#type' => 'select',
'#title' => $this->t('Comments view mode'),
'#description' => $this->t('Select the view mode used to show the list of comments.'),
'#default_value' => $this->getSetting('view_mode'),
'#options' => $view_modes,
// Only show the select element when there are more than one options.
'#access' => count($view_modes) > 1,
];
$element['pager_id'] = [
'#type' => 'select',
'#title' => $this->t('Pager ID'),
'#options' => range(0, 10),
'#default_value' => $this->getSetting('pager_id'),
'#description' => $this->t("Unless you're experiencing problems with pagers related to this field, you should leave this at 0. If using multiple pagers on one page you may need to set this number to a higher value so as not to conflict within the ?page= array. Large values will add a lot of commas to your URLs, so avoid if possible."),
];
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$view_mode = $this->getSetting('view_mode');
$view_modes = $this->getViewModes();
$view_mode_label = $view_modes[$view_mode] ?? 'default';
$summary = [$this->t('Comment view mode: @mode', ['@mode' => $view_mode_label])];
if ($pager_id = $this->getSetting('pager_id')) {
$summary[] = $this->t('Pager ID: @id', ['@id' => $pager_id]);
}
return $summary;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
if ($mode = $this->getSetting('view_mode')) {
if ($bundle = $this->getFieldSetting('comment_type')) {
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
if ($display = EntityViewDisplay::load("comment.$bundle.$mode")) {
$dependencies[$display->getConfigDependencyKey()][] = $display->getConfigDependencyName();
}
}
}
return $dependencies;
}
/**
* Provides a list of comment view modes for the configured comment type.
*
* @return array
* Associative array keyed by view mode key and having the view mode label
* as value.
*/
protected function getViewModes() {
return $this->entityDisplayRepository->getViewModeOptionsByBundle('comment', $this->getFieldSetting('comment_type'));
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Drupal\comment\Plugin\Field\FieldFormatter;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\StringFormatter;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'comment_permalink' formatter.
*
* All the other entities use 'canonical' or 'revision' links to link the entity
* to itself but comments use permalink URL.
*/
#[FieldFormatter(
id: 'comment_permalink',
label: new TranslatableMarkup('Comment Permalink'),
field_types: [
'string',
'uri',
],
)]
class CommentPermalinkFormatter extends StringFormatter {
/**
* {@inheritdoc}
*/
protected function getEntityUrl(EntityInterface $comment) {
/** @var \Drupal\comment\CommentInterface $comment */
$comment_permalink = $comment->permalink();
if ($comment->hasField('comment_body') && ($body = $comment->get('comment_body')->value)) {
$attributes = $comment_permalink->getOption('attributes') ?: [];
$attributes += ['title' => Unicode::truncate($body, 128)];
$comment_permalink->setOption('attributes', $attributes);
}
return $comment_permalink;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return parent::isApplicable($field_definition) && $field_definition->getTargetEntityTypeId() === 'comment' && $field_definition->getName() === 'subject';
}
}

View File

@@ -0,0 +1,215 @@
<?php
namespace Drupal\comment\Plugin\Field\FieldType;
use Drupal\comment\CommentFieldItemList;
use Drupal\comment\CommentInterface;
use Drupal\comment\CommentManagerInterface;
use Drupal\comment\Entity\CommentType;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\Url;
/**
* Plugin implementation of the 'comment' field type.
*/
#[FieldType(
id: "comment",
label: new TranslatableMarkup("Comments"),
description: new TranslatableMarkup("This field manages configuration and presentation of comments on an entity."),
default_widget: "comment_default",
default_formatter: "comment_default",
list_class: CommentFieldItemList::class,
cardinality: 1,
)]
class CommentItem extends FieldItemBase implements CommentItemInterface {
/**
* {@inheritdoc}
*/
public static function defaultStorageSettings() {
return [
'comment_type' => '',
] + parent::defaultStorageSettings();
}
/**
* {@inheritdoc}
*/
public static function defaultFieldSettings() {
return [
'default_mode' => CommentManagerInterface::COMMENT_MODE_THREADED,
'per_page' => 50,
'form_location' => CommentItemInterface::FORM_BELOW,
'anonymous' => CommentInterface::ANONYMOUS_MAYNOT_CONTACT,
'preview' => DRUPAL_OPTIONAL,
] + parent::defaultFieldSettings();
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['status'] = DataDefinition::create('integer')
->setLabel(new TranslatableMarkup('Comment status'))
->setRequired(TRUE);
$properties['cid'] = DataDefinition::create('integer')
->setLabel(new TranslatableMarkup('Last comment ID'));
$properties['last_comment_timestamp'] = DataDefinition::create('integer')
->setLabel(new TranslatableMarkup('Last comment timestamp'))
->setDescription(new TranslatableMarkup('The time that the last comment was created.'));
$properties['last_comment_name'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Last comment name'))
->setDescription(new TranslatableMarkup('The name of the user posting the last comment.'));
$properties['last_comment_uid'] = DataDefinition::create('integer')
->setLabel(new TranslatableMarkup('Last comment user ID'));
$properties['comment_count'] = DataDefinition::create('integer')
->setLabel(new TranslatableMarkup('Number of comments'))
->setDescription(new TranslatableMarkup('The number of comments.'));
return $properties;
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'status' => [
'description' => 'Whether comments are allowed on this entity: 0 = no, 1 = closed (read only), 2 = open (read/write).',
'type' => 'int',
'default' => 0,
],
],
'indexes' => [],
'foreign keys' => [],
];
}
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
$element = [];
$settings = $this->getSettings();
$anonymous_user = new AnonymousUserSession();
$element['default_mode'] = [
'#type' => 'checkbox',
'#title' => $this->t('Threading'),
'#default_value' => $settings['default_mode'],
'#description' => $this->t('Show comment replies in a threaded list.'),
];
$element['per_page'] = [
'#type' => 'number',
'#title' => $this->t('Comments per page'),
'#default_value' => $settings['per_page'],
'#required' => TRUE,
'#min' => 0,
'#max' => 1000,
'#description' => $this->t('Enter 0 to show all comments.'),
];
$element['anonymous'] = [
'#type' => 'select',
'#title' => $this->t('Anonymous commenting'),
'#default_value' => $settings['anonymous'],
'#options' => [
CommentInterface::ANONYMOUS_MAYNOT_CONTACT => $this->t('Anonymous posters may not enter their contact information'),
CommentInterface::ANONYMOUS_MAY_CONTACT => $this->t('Anonymous posters may leave their contact information'),
CommentInterface::ANONYMOUS_MUST_CONTACT => $this->t('Anonymous posters must leave their contact information'),
],
'#access' => $anonymous_user->hasPermission('post comments'),
];
$element['form_location'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show reply form on the same page as comments'),
'#default_value' => $settings['form_location'],
];
$element['preview'] = [
'#type' => 'radios',
'#title' => $this->t('Preview comment'),
'#default_value' => $settings['preview'],
'#options' => [
DRUPAL_DISABLED => $this->t('Disabled'),
DRUPAL_OPTIONAL => $this->t('Optional'),
DRUPAL_REQUIRED => $this->t('Required'),
],
];
return $element;
}
/**
* {@inheritdoc}
*/
public static function mainPropertyName() {
return 'status';
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
// There is always a value for this field, it is one of
// CommentItemInterface::OPEN, CommentItemInterface::CLOSED or
// CommentItemInterface::HIDDEN.
return FALSE;
}
/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
$element = [];
// @todo Inject entity storage once typed-data supports container injection.
// See https://www.drupal.org/node/2053415 for more details.
$comment_types = CommentType::loadMultiple();
$options = [];
$entity_type = $this->getEntity()->getEntityTypeId();
foreach ($comment_types as $comment_type) {
if ($comment_type->getTargetEntityTypeId() == $entity_type) {
$options[$comment_type->id()] = $comment_type->label();
}
}
$element['comment_type'] = [
'#type' => 'select',
'#title' => $this->t('Comment type'),
'#options' => $options,
'#required' => TRUE,
'#description' => $this->t('Select the Comment type to use for this comment field. Manage the comment types from the <a href=":url">administration overview page</a>.', [':url' => Url::fromRoute('entity.comment_type.collection')->toString()]),
'#default_value' => $this->getSetting('comment_type'),
'#disabled' => $has_data,
];
return $element;
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
$statuses = [
CommentItemInterface::HIDDEN,
CommentItemInterface::CLOSED,
CommentItemInterface::OPEN,
];
return [
'status' => $statuses[mt_rand(0, count($statuses) - 1)],
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\comment\Plugin\Field\FieldType;
/**
* Interface definition for Comment items.
*/
interface CommentItemInterface {
/**
* Comments for this entity are hidden.
*/
const HIDDEN = 0;
/**
* Comments for this entity are closed.
*/
const CLOSED = 1;
/**
* Comments for this entity are open.
*/
const OPEN = 2;
/**
* Comment form should be displayed on a separate page.
*/
const FORM_SEPARATE_PAGE = 0;
/**
* Comment form should be shown below post or list of comments.
*/
const FORM_BELOW = 1;
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Drupal\comment\Plugin\Field\FieldWidget;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a default comment widget.
*/
#[FieldWidget(
id: 'comment_default',
label: new TranslatableMarkup('Comment'),
field_types: ['comment'],
)]
class CommentWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$entity = $items->getEntity();
$element['status'] = [
'#type' => 'radios',
'#title' => $this->t('Comments'),
'#title_display' => 'invisible',
'#default_value' => $items->status,
'#required ' => TRUE,
'#options' => [
CommentItemInterface::OPEN => $this->t('Open'),
CommentItemInterface::CLOSED => $this->t('Closed'),
CommentItemInterface::HIDDEN => $this->t('Hidden'),
],
CommentItemInterface::OPEN => [
'#description' => $this->t('Users with the "Post comments" permission can post comments.'),
],
CommentItemInterface::CLOSED => [
'#description' => $this->t('Users cannot post comments, but existing comments will be displayed.'),
],
CommentItemInterface::HIDDEN => [
'#description' => $this->t('Comments and the comment form are hidden from view.'),
],
];
// If the entity doesn't have any comments, the "Closed" option makes no
// sense, so don't even bother presenting it to the user unless this is the
// default value widget on the field settings form.
if (!$this->isDefaultValueWidget($form_state) && !$items->comment_count) {
// Only hide the option when it's not the currently selected option.
if ($element['status']['#default_value'] !== CommentItemInterface::CLOSED) {
$element['status'][CommentItemInterface::CLOSED]['#access'] = FALSE;
}
}
// If the advanced settings tabs-set is available (normally rendered in the
// second column on wide-resolutions), place the field as a details element
// in this tab-set.
if (isset($form['advanced'])) {
// Get default value from the field.
$field_default_values = $this->fieldDefinition->getDefaultValue($entity);
// Override widget title to be helpful for end users.
$element['#title'] = $this->t('Comment settings');
$element += [
'#type' => 'details',
// Open the details when the selected value is different to the stored
// default values for the field.
'#open' => ($items->status != $field_default_values[0]['status']),
'#group' => 'advanced',
'#attributes' => [
'class' => ['comment-' . Html::getClass($entity->getEntityTypeId()) . '-settings-form'],
],
'#attached' => [
'library' => ['comment/drupal.comment'],
],
];
}
return $element;
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
// Add default values for statistics properties because we don't want to
// have them in form.
foreach ($values as &$value) {
$value += [
'cid' => 0,
'last_comment_timestamp' => 0,
'last_comment_name' => '',
'last_comment_uid' => 0,
'comment_count' => 0,
];
}
return $values;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Drupal\comment\Plugin\Menu\LocalTask;
use Drupal\comment\CommentStorageInterface;
use Drupal\Core\Menu\LocalTaskDefault;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides a local task that shows the amount of unapproved comments.
*/
class UnapprovedComments extends LocalTaskDefault implements ContainerFactoryPluginInterface {
use StringTranslationTrait;
/**
* The comment storage service.
*
* @var \Drupal\comment\CommentStorageInterface
*/
protected $commentStorage;
/**
* Construct the UnapprovedComments object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\comment\CommentStorageInterface $comment_storage
* The comment storage service.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, CommentStorageInterface $comment_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->commentStorage = $comment_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('comment')
);
}
/**
* {@inheritdoc}
*/
public function getTitle(?Request $request = NULL) {
return $this->t('Unapproved comments (@count)', ['@count' => $this->commentStorage->getUnapprovedCount()]);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Drupal\comment\Plugin\Validation\Constraint;
use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
/**
* Supports validating comment author names.
*/
#[Constraint(
id: 'CommentName',
label: new TranslatableMarkup('Comment author name', [], ['context' => 'Validation']),
type: 'entity:comment'
)]
class CommentNameConstraint extends CompositeConstraintBase {
/**
* Message shown when an anonymous user comments using a registered name.
*
* @var string
*/
public $messageNameTaken = 'The name you used (%name) belongs to a registered user.';
/**
* Message shown when an admin changes the comment-author to an invalid user.
*
* @var string
*/
public $messageRequired = 'You have to specify a valid author.';
/**
* Message shown when the name doesn't match the author's name.
*
* @var string
*/
public $messageMatch = 'The specified author name does not match the comment author.';
/**
* {@inheritdoc}
*/
public function coversFields() {
return ['name', 'uid'];
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Drupal\comment\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\comment\CommentInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the CommentName constraint.
*/
class CommentNameConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* User storage handler.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* Constructs a new CommentNameConstraintValidator.
*
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage handler.
*/
public function __construct(UserStorageInterface $user_storage) {
$this->userStorage = $user_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('entity_type.manager')->getStorage('user'));
}
/**
* {@inheritdoc}
*/
public function validate($entity, Constraint $constraint) {
$author_name = $entity->name->value;
$owner_id = (int) $entity->uid->target_id;
// Do not allow unauthenticated comment authors to use a name that is
// taken by a registered user.
if (isset($author_name) && $author_name !== '' && $owner_id === 0) {
$users = $this->userStorage->loadByProperties(['name' => $author_name]);
if (!empty($users)) {
$this->context->buildViolation($constraint->messageNameTaken, ['%name' => $author_name])
->atPath('name')
->addViolation();
}
}
// If an author name and owner are given, make sure they match.
elseif (isset($author_name) && $author_name !== '' && $owner_id) {
$owner = $this->userStorage->load($owner_id);
if ($owner->getAccountName() != $author_name) {
$this->context->buildViolation($constraint->messageMatch)
->atPath('name')
->addViolation();
}
}
// Anonymous account might be required - depending on field settings. We
// can't validate this without a valid commented entity, which will fail
// the validation elsewhere.
if ($owner_id === 0 && empty($author_name) && $entity->getCommentedEntity() && $entity->getFieldName() &&
$this->getAnonymousContactDetailsSetting($entity) === CommentInterface::ANONYMOUS_MUST_CONTACT) {
$this->context->buildViolation($constraint->messageRequired)
->atPath('name')
->addViolation();
}
}
/**
* Gets the anonymous contact details setting from the comment.
*
* @param \Drupal\comment\CommentInterface $comment
* The entity.
*
* @return int
* The anonymous contact setting.
*/
protected function getAnonymousContactDetailsSetting(CommentInterface $comment) {
return $comment
->getCommentedEntity()
->get($comment->getFieldName())
->getFieldDefinition()
->getSetting('anonymous');
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Drupal\comment\Plugin\migrate;
use Drupal\migrate_drupal\Plugin\migrate\FieldMigration;
/**
* Migration plugin for Drupal 7 comments with fields.
*/
class D7Comment extends FieldMigration {
/**
* {@inheritdoc}
*/
public function getProcess() {
if (!$this->init) {
$this->init = TRUE;
$this->fieldDiscovery->addEntityFieldProcesses($this, 'comment');
}
return parent::getProcess();
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Drupal\comment\Plugin\migrate\destination;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Comment entity destination.
*/
#[MigrateDestination('entity:comment')]
class EntityComment extends EntityContentBase {
/**
* The state storage object.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* An array of entity IDs for the 'commented entity' keyed by entity type.
*
* @var array
*/
protected $stubCommentedEntityIds;
/**
* Builds a comment entity destination.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager service.
* @param \Drupal\Core\State\StateInterface $state
* The state storage object.
* @param \Drupal\Core\Session\AccountSwitcherInterface|null $account_switcher
* The account switcher service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, StateInterface $state, ?AccountSwitcherInterface $account_switcher = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_field_manager, $field_type_manager, $account_switcher);
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
$entity_type = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity_type.manager')->getStorage($entity_type),
array_keys($container->get('entity_type.bundle.info')->getBundleInfo($entity_type)),
$container->get('entity_field.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('state'),
$container->get('account_switcher')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
if ($row->isStub() && ($state = $this->state->get('comment.maintain_entity_statistics', 0))) {
$this->state->set('comment.maintain_entity_statistics', 0);
}
$return = parent::import($row, $old_destination_id_values);
if ($row->isStub() && $state) {
$this->state->set('comment.maintain_entity_statistics', $state);
}
return $return;
}
/**
* {@inheritdoc}
*/
protected function processStubRow(Row $row) {
parent::processStubRow($row);
// Neither uid nor name is required in itself, but it is required to set one
// of them.
$row->setDestinationProperty('name', 'anonymous_stub');
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Drupal\comment\Plugin\migrate\destination;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase;
use Drupal\migrate\Row;
/**
* Comment type destination.
*/
#[MigrateDestination('entity:comment_type')]
class EntityCommentType extends EntityConfigBase {
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
$entity_ids = parent::import($row, $old_destination_id_values);
\Drupal::service('comment.manager')->addBodyField(reset($entity_ids));
return $entity_ids;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Drupal\comment\Plugin\migrate\source;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6/7 comment types source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "comment_type",
* source_module = "comment"
* )
*/
class CommentType extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('node_type', 't')
->fields('t', ['type', 'name']);
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$node_type = $row->getSourceProperty('type');
foreach (array_keys($this->getCommentFields()) as $field) {
$row->setSourceProperty($field, $this->variableGet($field . '_' . $node_type, NULL));
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'name' => $this->t('Human name of the parent node type.'),
'type' => $this->t('Machine name of the parent node type.'),
] + $this->getCommentFields();
}
/**
* Returns the fields containing comment settings for each node type.
*
* @return string[]
* An associative array of field descriptions, keyed by field.
*/
protected function getCommentFields() {
return [
'comment' => $this->t('Default comment setting'),
'comment_default_mode' => $this->t('Default display mode'),
'comment_default_per_page' => $this->t('Default comments per page'),
'comment_anonymous' => $this->t('Anonymous commenting'),
'comment_subject_field' => $this->t('Comment subject field'),
'comment_preview' => $this->t('Preview comment'),
'comment_form_location' => $this->t('Location of comment submission form'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['type']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function checkRequirements() {
parent::checkRequirements();
if (!$this->moduleExists('node')) {
// Drupal 6 and Drupal 7 comment configuration migrations migrate comment
// types and comment fields for node comments only.
throw new RequirementsException('The node module is not enabled in the source site.', [
'source_module_additional' => 'node',
]);
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Drupal\comment\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
// cspell:ignore vancode
/**
* Drupal 6 comment source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_comment",
* source_module = "comment"
* )
*/
class Comment extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('comments', 'c')
->fields('c', ['cid', 'pid', 'nid', 'uid', 'subject', 'comment',
'hostname', 'timestamp', 'status', 'thread', 'name', 'mail', 'homepage',
'format',
]);
$query->innerJoin('node', 'n', '[c].[nid] = [n].[nid]');
$query->fields('n', ['type', 'language']);
$query->orderBy('c.timestamp');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// In D6, status=0 means published, while in D8 means the opposite.
$row->setSourceProperty('status', !$row->getSourceProperty('status'));
// If node did not have a language, use site default language as a fallback.
if (!$row->getSourceProperty('language')) {
$language_default = $this->variableGet('language_default', NULL);
$language = $language_default ? $language_default->language : 'en';
$row->setSourceProperty('language', $language);
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'cid' => $this->t('Comment ID.'),
'pid' => $this->t('Parent comment ID. If set to 0, this comment is not a reply to an existing comment.'),
'nid' => $this->t('The {node}.nid to which this comment is a reply.'),
'uid' => $this->t('The {users}.uid who authored the comment. If set to 0, this comment was created by an anonymous user.'),
'subject' => $this->t('The comment title.'),
'comment' => $this->t('The comment body.'),
'hostname' => $this->t("The author's host name."),
'timestamp' => $this->t('The time that the comment was created, or last edited by its author, as a Unix timestamp.'),
'status' => $this->t('The published status of a comment. (0 = Published, 1 = Not Published)'),
'format' => $this->t('The {filter_formats}.format of the comment body.'),
'thread' => $this->t("The vancode representation of the comment's place in a thread."),
'name' => $this->t("The comment author's name. Uses {users}.name if the user is logged in, otherwise uses the value typed into the comment form."),
'mail' => $this->t("The comment author's email address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on."),
'homepage' => $this->t("The comment author's home page address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on."),
'type' => $this->t("The {node}.type to which this comment is a reply."),
'language' => $this->t("The {node}.language to which this comment is a reply. Site default language is used as a fallback if node does not have a language."),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['cid']['type'] = 'integer';
return $ids;
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Drupal\comment\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
// cspell:ignore vancode
/**
* Drupal 7 comment source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_comment",
* source_module = "comment"
* )
*/
class Comment extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('comment', 'c')->fields('c');
$query->innerJoin('node', 'n', '[c].[nid] = [n].[nid]');
$query->addField('n', 'type', 'node_type');
$query->orderBy('c.created');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$cid = $row->getSourceProperty('cid');
$node_type = $row->getSourceProperty('node_type');
$comment_type = 'comment_node_' . $node_type;
$row->setSourceProperty('comment_type', 'comment_node_' . $node_type);
// If this entity was translated using Entity Translation, we need to get
// its source language to get the field values in the right language.
// The translations will be migrated by the d7_comment_entity_translation
// migration.
$entity_translatable = $this->isEntityTranslatable('comment') && (int) $this->variableGet('language_content_type_' . $node_type, 0) === 4;
$source_language = $this->getEntityTranslationSourceLanguage('comment', $cid);
$language = $entity_translatable && $source_language ? $source_language : $row->getSourceProperty('language');
// Get Field API field values.
foreach ($this->getFields('comment', $comment_type) as $field_name => $field) {
// Ensure we're using the right language if the entity and the field are
// translatable.
$field_language = $entity_translatable && $field['translatable'] ? $language : NULL;
$row->setSourceProperty($field_name, $this->getFieldValues('comment', $field_name, $cid, NULL, $field_language));
}
// If the comment subject was replaced by a real field using the Drupal 7
// Title module, use the field value instead of the comment subject.
if ($this->moduleExists('title')) {
$subject_field = $row->getSourceProperty('subject_field');
if (isset($subject_field[0]['value'])) {
$row->setSourceProperty('subject', $subject_field[0]['value']);
}
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'cid' => $this->t('Comment ID.'),
'pid' => $this->t('Parent comment ID. If set to 0, this comment is not a reply to an existing comment.'),
'nid' => $this->t('The {node}.nid to which this comment is a reply.'),
'uid' => $this->t('The {users}.uid who authored the comment. If set to 0, this comment was created by an anonymous user.'),
'subject' => $this->t('The comment title.'),
'comment' => $this->t('The comment body.'),
'hostname' => $this->t("The author's host name."),
'created' => $this->t('The time that the comment was created, as a Unix timestamp.'),
'changed' => $this->t('The time that the comment was edited by its author, as a Unix timestamp.'),
'status' => $this->t('The published status of a comment. (0 = Published, 1 = Not Published)'),
'format' => $this->t('The {filter_formats}.format of the comment body.'),
'thread' => $this->t("The vancode representation of the comment's place in a thread."),
'name' => $this->t("The comment author's name. Uses {users}.name if the user is logged in, otherwise uses the value typed into the comment form."),
'mail' => $this->t("The comment author's email address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on."),
'homepage' => $this->t("The comment author's home page address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on."),
'language' => $this->t('The comment language.'),
'type' => $this->t("The {node}.type to which this comment is a reply."),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['cid']['type'] = 'integer';
return $ids;
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Drupal\comment\Plugin\migrate\source\d7;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
/**
* Drupal 7 comment entity translation source plugin.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_comment_entity_translation",
* source_module = "entity_translation"
* )
*/
class CommentEntityTranslation extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('entity_translation', 'et')
->fields('et')
->fields('c', [
'subject',
])
->condition('et.entity_type', 'comment')
->condition('et.source', '', '<>');
$query->innerJoin('comment', 'c', '[c].[cid] = [et].[entity_id]');
$query->innerJoin('node', 'n', '[n].[nid] = [c].[nid]');
$query->addField('n', 'type', 'node_type');
$query->orderBy('et.created');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$cid = $row->getSourceProperty('entity_id');
$language = $row->getSourceProperty('language');
$node_type = $row->getSourceProperty('node_type');
$comment_type = 'comment_node_' . $node_type;
// Get Field API field values.
foreach ($this->getFields('comment', $comment_type) as $field_name => $field) {
// Ensure we're using the right language if the entity is translatable.
$field_language = $field['translatable'] ? $language : NULL;
$row->setSourceProperty($field_name, $this->getFieldValues('comment', $field_name, $cid, NULL, $field_language));
}
// If the comment subject was replaced by a real field using the Drupal 7
// Title module, use the field value instead of the comment subject.
if ($this->moduleExists('title')) {
$subject_field = $row->getSourceProperty('subject_field');
if (isset($subject_field[0]['value'])) {
$row->setSourceProperty('subject', $subject_field[0]['value']);
}
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'entity_type' => $this->t('The entity type this translation relates to'),
'entity_id' => $this->t('The entity ID this translation relates to'),
'revision_id' => $this->t('The entity revision ID this translation relates to'),
'language' => $this->t('The target language for this translation.'),
'source' => $this->t('The source language from which this translation was created.'),
'uid' => $this->t('The author of this translation.'),
'status' => $this->t('Boolean indicating whether the translation is published (visible to non-administrators).'),
'translate' => $this->t('A boolean indicating whether this translation needs to be updated.'),
'created' => $this->t('The Unix timestamp when the translation was created.'),
'changed' => $this->t('The Unix timestamp when the translation was most recently saved.'),
'subject' => $this->t('The comment title.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'entity_id' => [
'type' => 'integer',
'alias' => 'et',
],
'language' => [
'type' => 'string',
'alias' => 'et',
],
];
}
/**
* {@inheritdoc}
*/
public function checkRequirements() {
parent::checkRequirements();
if (!$this->moduleExists('comment')) {
// If we make it to here, the comment module isn't installed.
throw new RequirementsException('The module comment is not enabled in the source site');
}
if (!$this->moduleExists('node')) {
// Node module is also a requirement.
throw new RequirementsException('The module node is not enabled in the source site');
}
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Drupal\comment\Plugin\views\argument;
use Drupal\Core\Database\Connection;
use Drupal\views\Attribute\ViewsArgument;
use Drupal\views\Plugin\views\argument\ArgumentPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* The views user ID argument handler.
*
* Accepts a user ID to check for nodes that the user posted or commented on.
*
* @ingroup views_argument_handlers
*/
#[ViewsArgument(
id: 'argument_comment_user_uid',
)]
class UserUid extends ArgumentPluginBase {
/**
* Database Service Object.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Constructs a \Drupal\comment\Plugin\views\argument\UserUid object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Database\Connection $database
* Database Service Object.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container->get('database'));
}
public function title() {
if (!$this->argument) {
$title = \Drupal::config('user.settings')->get('anonymous');
}
else {
$title = $this->database->query('SELECT [name] FROM {users_field_data} WHERE [uid] = :uid AND [default_langcode] = 1', [':uid' => $this->argument])->fetchField();
}
if (empty($title)) {
return $this->t('No user');
}
return $title;
}
protected function defaultActions($which = NULL) {
// Disallow summary views on this argument.
if (!$which) {
$actions = parent::defaultActions();
unset($actions['summary asc']);
unset($actions['summary desc']);
return $actions;
}
if ($which != 'summary asc' && $which != 'summary desc') {
return parent::defaultActions($which);
}
}
public function query($group_by = FALSE) {
$this->ensureMyTable();
// Use the table definition to correctly add this user ID condition.
if ($this->table != 'comment_field_data') {
$subselect = $this->database->select('comment_field_data', 'c');
$subselect->addField('c', 'cid');
$subselect->condition('c.uid', $this->argument);
$entity_id = $this->definition['entity_id'];
$entity_type = $this->definition['entity_type'];
$subselect->where("[c].[entity_id] = [$this->tableAlias].[$entity_id]");
$subselect->condition('c.entity_type', $entity_type);
$condition = ($this->view->query->getConnection()->condition('OR'))
->condition("$this->tableAlias.uid", $this->argument, '=')
->exists($subselect);
$this->query->addWhere(0, $condition);
}
}
/**
* {@inheritdoc}
*/
public function getSortName() {
return $this->t('Numerical', [], ['context' => 'Sort order']);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Drupal\comment\Plugin\views\field;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\BulkForm;
/**
* Defines a comment operations bulk form element.
*/
#[ViewsField("comment_bulk_form")]
class CommentBulkForm extends BulkForm {
/**
* {@inheritdoc}
*/
protected function emptySelectedMessage() {
return $this->t('Select one or more comments to perform the update on.');
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\comment\Plugin\views\field;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\EntityField;
use Drupal\views\ResultRow;
/**
* Views field display for commented entity.
*/
#[ViewsField("commented_entity")]
class CommentedEntity extends EntityField {
/**
* Array of entities that has comments.
*
* We use this to load all the commented entities of same entity type at once
* to the EntityStorageController static cache.
*
* @var array
*/
protected $loadedCommentedEntities = [];
/**
* {@inheritdoc}
*/
public function getItems(ResultRow $values) {
if (empty($this->loadedCommentedEntities)) {
$result = $this->view->result;
$entity_ids_per_type = [];
foreach ($result as $value) {
/** @var \Drupal\comment\CommentInterface $comment */
if ($comment = $this->getEntity($value)) {
$entity_ids_per_type[$comment->getCommentedEntityTypeId()][] = $comment->getCommentedEntityId();
}
}
foreach ($entity_ids_per_type as $type => $ids) {
$this->loadedCommentedEntities[$type] = $this->entityTypeManager->getStorage($type)->loadMultiple($ids);
}
}
return parent::getItems($values);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\comment\Plugin\views\field;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\EntityField;
use Drupal\views\ResultRow;
/**
* Field handler to display the depth of a comment.
*
* @ingroup views_field_handlers
*/
#[ViewsField("comment_depth")]
class Depth extends EntityField {
/**
* {@inheritdoc}
*/
public function getItems(ResultRow $values) {
$items = parent::getItems($values);
foreach ($items as &$item) {
// Work out the depth of this comment.
$comment_thread = $item['rendered']['#context']['value'];
$item['rendered']['#context']['value'] = count(explode('.', $comment_thread)) - 1;
}
return $items;
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Drupal\comment\Plugin\views\field;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;
/**
* Handler for showing comment module's entity links.
*
* @ingroup views_field_handlers
*/
#[ViewsField("comment_entity_link")]
class EntityLink extends FieldPluginBase {
/**
* Stores the result of parent entities build for all rows to reuse it later.
*
* @var array
*/
protected $build;
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['teaser'] = ['default' => FALSE];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['teaser'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show teaser-style link'),
'#default_value' => $this->options['teaser'],
'#description' => $this->t('Show the comment link in the form used on standard entity teasers, rather than the full entity form.'),
];
parent::buildOptionsForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function query() {}
/**
* {@inheritdoc}
*/
public function preRender(&$values) {
// Render all nodes, so you can grep the comment links.
$entities = [];
foreach ($values as $row) {
$entity = $row->_entity;
$entities[$entity->id()] = $entity;
}
if ($entities) {
$entityTypeId = reset($entities)->getEntityTypeId();
$viewMode = $this->options['teaser'] ? 'teaser' : 'full';
$this->build = \Drupal::entityTypeManager()
->getViewBuilder($entityTypeId)
->viewMultiple($entities, $viewMode);
}
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
$entity = $this->getEntity($values);
// Only render the links, if they are defined.
if (!$entity || empty($this->build[$entity->id()]['links']['comment__comment'])) {
return '';
}
return \Drupal::service('renderer')->render($this->build[$entity->id()]['links']['comment__comment']);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Drupal\comment\Plugin\views\field;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\Date;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* Field handler to display the timestamp of a comment with the count of comments.
*
* @ingroup views_field_handlers
*/
#[ViewsField("comment_last_timestamp")]
class LastTimestamp extends Date {
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
parent::init($view, $display, $options);
$this->additional_fields['comment_count'] = 'comment_count';
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
$comment_count = $this->getValue($values, 'comment_count');
if (empty($this->options['empty_zero']) || $comment_count) {
return parent::render($values);
}
else {
return NULL;
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\comment\Plugin\views\field;
use Drupal\Core\Url;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\LinkBase;
use Drupal\views\ResultRow;
/**
* Provides a comment approve link.
*
* @ingroup views_field_handlers
*/
#[ViewsField("comment_link_approve")]
class LinkApprove extends LinkBase {
/**
* {@inheritdoc}
*/
protected function getUrlInfo(ResultRow $row) {
$entity = $this->getEntity($row);
if (!$entity) {
return NULL;
}
return Url::fromRoute('comment.approve', ['comment' => $entity->id()]);
}
/**
* {@inheritdoc}
*/
protected function renderLink(ResultRow $row) {
$this->options['alter']['query'] = $this->getDestinationArray();
return parent::renderLink($row);
}
/**
* {@inheritdoc}
*/
protected function getDefaultLabel() {
return $this->t('Approve');
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Drupal\comment\Plugin\views\field;
use Drupal\Core\Url;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\LinkBase;
use Drupal\views\ResultRow;
/**
* Field handler to present a link to reply to a comment.
*
* @ingroup views_field_handlers
*/
#[ViewsField("comment_link_reply")]
class LinkReply extends LinkBase {
/**
* {@inheritdoc}
*/
protected function getUrlInfo(ResultRow $row) {
/** @var \Drupal\comment\CommentInterface $comment */
$comment = $this->getEntity($row);
if (!$comment) {
return NULL;
}
return Url::fromRoute('comment.reply', [
'entity_type' => $comment->getCommentedEntityTypeId(),
'entity' => $comment->getCommentedEntityId(),
'field_name' => $comment->getFieldName(),
'pid' => $comment->id(),
]);
}
/**
* {@inheritdoc}
*/
protected function getDefaultLabel() {
return $this->t('Reply');
}
}

View File

@@ -0,0 +1,231 @@
<?php
namespace Drupal\comment\Plugin\views\field;
use Drupal\Core\Database\Connection;
use Drupal\comment\CommentInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\node\Entity\Node;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\NumericField;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Field handler to display the number of new comments.
*
* @ingroup views_field_handlers
*/
#[ViewsField("node_new_comments")]
class NodeNewComments extends NumericField {
/**
* {@inheritdoc}
*/
public function usesGroupBy() {
return FALSE;
}
/**
* Database Service Object.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* Constructs a \Drupal\comment\Plugin\views\field\NodeNewComments object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Database\Connection $database
* Database Service Object.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->database = $database;
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('database'),
$container->get('entity_type.manager'),
$container->get('entity_field.manager')
);
}
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
parent::init($view, $display, $options);
$this->additional_fields['entity_id'] = 'nid';
$this->additional_fields['type'] = 'type';
$this->additional_fields['comment_count'] = ['table' => 'comment_entity_statistics', 'field' => 'comment_count'];
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['link_to_comment'] = ['default' => TRUE];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['link_to_comment'] = [
'#title' => $this->t('Link this field to new comments'),
'#description' => $this->t("Enable to override this field's links."),
'#type' => 'checkbox',
'#default_value' => $this->options['link_to_comment'],
];
parent::buildOptionsForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function query() {
$this->ensureMyTable();
$this->addAdditionalFields();
$this->field_alias = $this->table . '_' . $this->field;
}
/**
* {@inheritdoc}
*/
public function preRender(&$values) {
$user = \Drupal::currentUser();
if ($user->isAnonymous() || empty($values)) {
return;
}
$nids = [];
$ids = [];
foreach ($values as $id => $result) {
$nids[] = $result->{$this->aliases['nid']};
$values[$id]->{$this->field_alias} = 0;
// Create a reference so we can find this record in the values again.
if (empty($ids[$result->{$this->aliases['nid']}])) {
$ids[$result->{$this->aliases['nid']}] = [];
}
$ids[$result->{$this->aliases['nid']}][] = $id;
}
if ($nids) {
$result = $this->database->query("SELECT [n].[nid], COUNT([c].[cid]) AS [num_comments] FROM {node} [n] INNER JOIN {comment_field_data} [c] ON [n].[nid] = [c].[entity_id] AND [c].[entity_type] = 'node' AND [c].[default_langcode] = 1
LEFT JOIN {history} [h] ON [h].[nid] = [n].[nid] AND [h].[uid] = :h_uid WHERE [n].[nid] IN ( :nids[] )
AND [c].[changed] > GREATEST(COALESCE([h].[timestamp], :timestamp1), :timestamp2) AND [c].[status] = :status GROUP BY [n].[nid]", [
':status' => CommentInterface::PUBLISHED,
':h_uid' => $user->id(),
':nids[]' => $nids,
':timestamp1' => HISTORY_READ_LIMIT,
':timestamp2' => HISTORY_READ_LIMIT,
]);
foreach ($result as $node) {
foreach ($ids[$node->nid] as $id) {
$values[$id]->{$this->field_alias} = $node->num_comments;
}
}
}
}
/**
* Prepares the link to the first new comment.
*
* @param string $data
* The XSS safe string for the link text.
* @param \Drupal\views\ResultRow $values
* The values retrieved from a single row of a view's query result.
*
* @return string
* Returns a string for the link text.
*/
protected function renderLink($data, ResultRow $values) {
if (!empty($this->options['link_to_comment']) && $data !== NULL && $data !== '') {
$node_type = $this->getValue($values, 'type');
$node = Node::create([
'nid' => $this->getValue($values, 'nid'),
'type' => $node_type,
]);
// Because there is no support for selecting a specific comment field to
// reference, we arbitrarily use the first such field name we find.
// @todo Provide a means for selecting the comment field.
// https://www.drupal.org/node/2594201
$field_map = $this->entityFieldManager->getFieldMapByFieldType('comment');
$comment_field_name = 'comment';
foreach ($field_map['node'] as $field_name => $field_data) {
foreach ($field_data['bundles'] as $bundle_name) {
if ($node_type == $bundle_name) {
$comment_field_name = $field_name;
break 2;
}
}
}
$page_number = $this->entityTypeManager->getStorage('comment')
->getNewCommentPageNumber($this->getValue($values, 'comment_count'), $this->getValue($values), $node, $comment_field_name);
$this->options['alter']['make_link'] = TRUE;
$this->options['alter']['url'] = $node->toUrl();
$this->options['alter']['query'] = $page_number ? ['page' => $page_number] : NULL;
$this->options['alter']['fragment'] = 'new';
}
return $data;
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
$value = $this->getValue($values);
if (!empty($value)) {
return $this->renderLink(parent::render($values), $values);
}
else {
$this->options['alter']['make_link'] = FALSE;
}
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Drupal\comment\Plugin\views\field;
use Drupal\user\Entity\User;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;
/**
* Field handler to present the name of the last comment poster.
*
* @ingroup views_field_handlers
*/
#[ViewsField("comment_ces_last_comment_name")]
class StatisticsLastCommentName extends FieldPluginBase {
/**
* The users table.
*/
// phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName
protected ?string $user_table;
/**
* The user name field.
*/
// phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName
protected string $user_field;
/**
* The user id.
*/
public string $uid;
/**
* {@inheritdoc}
*/
public function query() {
// last_comment_name only contains data if the user is anonymous. So we
// have to join in a specially related user table.
$this->ensureMyTable();
// join 'users' to this table via vid
$definition = [
'table' => 'users_field_data',
'field' => 'uid',
'left_table' => 'comment_entity_statistics',
'left_field' => 'last_comment_uid',
'extra' => [
[
'field' => 'uid',
'operator' => '!=',
'value' => '0',
],
],
];
$join = \Drupal::service('plugin.manager.views.join')->createInstance('standard', $definition);
// nes_user alias so this can work with the sort handler, below.
$this->user_table = $this->query->ensureTable('ces_users', $this->relationship, $join);
$this->field_alias = $this->query->addField(NULL, "COALESCE($this->user_table.name, $this->tableAlias.$this->field)", $this->tableAlias . '_' . $this->field);
$this->user_field = $this->query->addField($this->user_table, 'name');
$this->uid = $this->query->addField($this->tableAlias, 'last_comment_uid');
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['link_to_user'] = ['default' => TRUE];
return $options;
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
if (!empty($this->options['link_to_user'])) {
$account = User::create();
$account->name = $this->getValue($values);
$account->uid = $values->{$this->uid};
$username = [
'#theme' => 'username',
'#account' => $account,
];
return \Drupal::service('renderer')->render($username);
}
else {
return $this->sanitizeValue($this->getValue($values));
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Drupal\comment\Plugin\views\field;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\Date;
/**
* Field handler to display the newer of last comment / node updated.
*
* @ingroup views_field_handlers
*/
#[ViewsField("comment_ces_last_updated")]
class StatisticsLastUpdated extends Date {
/**
* The node table.
*/
// phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName
protected ?string $node_table;
public function query() {
$this->ensureMyTable();
$this->node_table = $this->query->ensureTable('node_field_data', $this->relationship);
$this->field_alias = $this->query->addField(NULL, "GREATEST(" . $this->node_table . ".changed, " . $this->tableAlias . ".last_comment_timestamp)", $this->tableAlias . '_' . $this->field);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\comment\Plugin\views\filter;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\views\Attribute\ViewsFilter;
use Drupal\views\Plugin\views\filter\InOperator;
/**
* Filter based on comment node status.
*
* @ingroup views_filter_handlers
*/
#[ViewsFilter("node_comment")]
class NodeComment extends InOperator {
/**
* {@inheritdoc}
*/
public function getValueOptions() {
if (!isset($this->valueOptions)) {
$this->valueOptions = [
CommentItemInterface::HIDDEN => $this->t('Hidden'),
CommentItemInterface::CLOSED => $this->t('Closed'),
CommentItemInterface::OPEN => $this->t('Open'),
];
}
return $this->valueOptions;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Drupal\comment\Plugin\views\filter;
use Drupal\views\Attribute\ViewsFilter;
use Drupal\views\Plugin\views\filter\Date;
/**
* Filter handler for the newer of last comment / node updated.
*
* @ingroup views_filter_handlers
*/
#[ViewsFilter("comment_ces_last_updated")]
class StatisticsLastUpdated extends Date {
/**
* The node table.
*/
// phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName
protected ?string $node_table;
public function query() {
$this->ensureMyTable();
$this->node_table = $this->query->ensureTable('node', $this->relationship);
$field = "GREATEST(" . $this->node_table . ".changed, " . $this->tableAlias . ".last_comment_timestamp)";
$info = $this->operators();
if (!empty($info[$this->operator]['method'])) {
$this->{$info[$this->operator]['method']}($field);
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Drupal\comment\Plugin\views\filter;
use Drupal\Core\Database\Database;
use Drupal\views\Attribute\ViewsFilter;
use Drupal\views\Plugin\views\filter\FilterPluginBase;
/**
* Filter handler, accepts user ID to check for nodes user posted/commented on.
*
* @ingroup views_filter_handlers
*/
#[ViewsFilter("comment_user_uid")]
class UserUid extends FilterPluginBase {
public function query() {
$this->ensureMyTable();
$subselect = Database::getConnection()->select('comment_field_data', 'c');
$subselect->addField('c', 'cid');
$subselect->condition('c.uid', $this->value, $this->operator);
$entity_id = $this->definition['entity_id'];
$entity_type = $this->definition['entity_type'];
$subselect->where("[c].[entity_id] = [$this->tableAlias].[$entity_id]");
$subselect->condition('c.entity_type', $entity_type);
$condition = ($this->view->query->getConnection()->condition('OR'))
->condition("$this->tableAlias.uid", $this->value, $this->operator)
->exists($subselect);
$this->query->addWhere($this->options['group'], $condition);
}
}

Some files were not shown because too many files have changed in this diff Show More