Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,23 @@
/**
* @file
* Attaches comment behaviors to the entity form.
*/
(function ($) {
"use strict";
/**
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.commentFieldsetSummaries = {
attach: function (context) {
var $context = $(context);
$context.find('fieldset.comment-entity-settings-form').drupalSetSummary(function (context) {
return Drupal.checkPlain($(context).find('.form-item-comment input:checked').next('label').text());
});
}
};
})(jQuery);

View file

@ -0,0 +1,49 @@
<?php
use Drupal\Core\Entity\EntityInterface;
use Drupal\comment\CommentInterface;
use Drupal\Core\Url;
/**
* @file
* Hooks provided by the Comment module.
*/
/**
* @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['mymodule'] = array(
'#theme' => 'links__comment__mymodule',
'#attributes' => array('class' => array('links', 'inline')),
'#links' => array(
'comment-report' => array(
'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,9 @@
name: Comment
type: module
description: 'Allows users to comment on and discuss published content.'
package: Core
version: VERSION
core: 8.x
dependencies:
- text
configure: comment.admin

View file

@ -0,0 +1,112 @@
<?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.
$fields = entity_load_multiple_by_properties('field_storage_config', array('type' => 'comment'));
foreach ($fields as $field) {
$field->delete();
}
// 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'] = array(
'description' => 'Maintains statistics of entity and comments posts to show "new" and "updated" flags.',
'fields' => array(
'entity_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'The entity_id of the entity for which the statistics are compiled.',
),
'entity_type' => array(
'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' => array(
'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' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The {comment}.cid of the last comment.',
),
'last_comment_timestamp' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.changed.',
),
'last_comment_name' => array(
'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' => array(
'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' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'The total number of comments on this entity.',
),
),
'primary key' => array('entity_id', 'entity_type', 'field_name'),
'indexes' => array(
'last_comment_timestamp' => array('last_comment_timestamp'),
'comment_count' => array('comment_count'),
'last_comment_uid' => array('last_comment_uid'),
),
'foreign keys' => array(
'last_comment_author' => array(
'table' => 'users',
'columns' => array(
'last_comment_uid' => 'uid',
),
),
),
);
return $schema;
}

View file

@ -0,0 +1,38 @@
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/jquery.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/jquery.once
- core/drupal
- history/api

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,37 @@
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
# 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,773 @@
<?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 forum topic, story, collaborative
* book page, user etc.
*/
use Drupal\comment\CommentInterface;
use Drupal\comment\Entity\Comment;
use Drupal\comment\CommentManagerInterface;
use Drupal\comment\Entity\CommentType;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\FieldConfigInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\file\FileInterface;
use Drupal\user\EntityOwnerInterface;
use Drupal\node\NodeInterface;
use Drupal\user\RoleInterface;
/**
* Anonymous posters cannot enter their contact information.
*/
const COMMENT_ANONYMOUS_MAYNOT_CONTACT = 0;
/**
* Anonymous posters may leave their contact information.
*/
const COMMENT_ANONYMOUS_MAY_CONTACT = 1;
/**
* Anonymous posters are required to leave their contact information.
*/
const COMMENT_ANONYMOUS_MUST_CONTACT = 2;
/**
* 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/1029708 lands.
*/
define('COMMENT_NEW_LIMIT', 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 = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Comment module allows users to comment on site content, set commenting defaults and permissions, and moderate comments. For more information, see the <a href="!comment">online documentation for the Comment module</a>.', array('!comment' => 'https://www.drupal.org/documentation/modules/comment')) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Enabling commenting and configuring defaults') . '</dt>';
$output .= '<dd>' . t('Comment functionality can be enabled for any entity sub-type (for example, a <a href="!content-type">content type</a>). On the Manage fields page for each entity sub-type, you can enable commenting by adding a Comments field. The entity sub-types each have their own default comment settings configured as: <em>Open</em> to allow new comments, <em>Closed</em> to view existing comments, but prevent new comments, or <em>Hidden</em> to hide existing comments and prevent new comments. For background information about entities, see the <a href="!field">Field module help page</a>.', array('!content-type' => (\Drupal::moduleHandler()->moduleExists('node')) ? \Drupal::url('entity.node_type.collection') : '#', '!field' => \Drupal::url('help.page', array('name' => 'field')))) . '</dd>';
$output .= '<dt>' . t('Overriding default settings') . '</dt>';
$output .= '<dd>' . t('When you create an entity item, you can override the default comment settings. Changing the entity sub-type defaults will not affect existing entity items, whether they used the default settings or had overrides.') . '</dd>';
$output .= '<dt>' . t('Approving and managing comments') . '</dt>';
$output .= '<dd>' . t('Comments from users who have the <em>Skip comment approval</em> permission are published immediately. All other comments are placed in the <a href="!comment-approval">Unapproved comments</a> queue, until a user who has permission to <em>Administer comments and comment settings</em> publishes or deletes them. Published comments can be bulk managed on the <a href="!admin-comment">Published comments</a> administration page. When a comment has no replies, it remains editable by its author, as long as the author has <em>Edit own comments</em> permission.', array('!comment-approval' => \Drupal::url('comment.admin_approval'), '!admin-comment' => \Drupal::url('comment.admin'))) . '</dd>';
$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',
array(
'comment' => $comment->id(),
),
array('fragment' => 'comment-' . $comment->id())
);
}
/**
* Implements hook_entity_extra_field_info().
*/
function comment_entity_extra_field_info() {
$return = array();
foreach (CommentType::loadMultiple() as $comment_type) {
$return['comment'][$comment_type->id()] = array(
'form' => array(
'author' => array(
'label' => t('Author'),
'description' => t('Author textfield'),
'weight' => -2,
),
),
);
$return['comment'][$comment_type->id()]['display']['links'] = array(
'label' => t('Links'),
'description' => t('Comment operation links'),
'weight' => 100,
'visible' => TRUE,
);
}
return $return;
}
/**
* Implements hook_theme().
*/
function comment_theme() {
return array(
'comment' => array(
'render element' => 'elements',
),
'field__comment' => array(
'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.
if (!isset($field->default_value)) {
$field->setDefaultValue(array());
}
$field->default_value += array(array());
$field->default_value[0] += array(
'status' => CommentItemInterface::OPEN,
'cid' => 0,
'last_comment_timestamp' => 0,
'last_comment_name' => '',
'last_comment_uid' => 0,
'comment_count' => 0,
);
}
}
/**
* 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::entityManager()->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');
$entity_query->condition('entity_type', $field->getEntityTypeId());
$entity_query->condition('field_name', $field->getName());
$cids = $entity_query->execute();
entity_delete_multiple('comment', $cids);
}
}
/**
* Implements hook_node_links_alter().
*/
function comment_node_links_alter(array &$node_links, NodeInterface $node, array &$context) {
// Comment links are only added to node entity type for backwards
// compatibility. Should you require comment links for other entity types you
// can do so by implementing a new field formatter.
// @todo Make this configurable from the formatter. See
// https://www.drupal.org/node/1901110.
$links = \Drupal::service('comment.link_builder')->buildCommentedEntityLinks($node, $context);
$node_links += $links;
}
/**
* Implements hook_entity_view().
*/
function comment_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode, $langcode) {
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 = array(
'fragment' => 'comments',
'absolute' => TRUE,
);
$entity->rss_elements[] = array(
'key' => 'comments',
'value' => $entity->url('canonical', $options),
);
}
}
}
}
/**
* 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();
}
}
/**
* Generates an array for rendering a comment.
*
* @param \Drupal\comment\CommentInterface $comment
* The comment object.
* @param $view_mode
* (optional) View mode; e.g., 'full', 'teaser', etc. Defaults to 'full'.
* @param $langcode
* (optional) A language code to use for rendering. Defaults to the global
* content language of the current request.
*
* @return array
* An array as expected by drupal_render().
*/
function comment_view(CommentInterface $comment, $view_mode = 'full', $langcode = NULL) {
return entity_view($comment, $view_mode, $langcode);
}
/**
* Constructs render array from an array of loaded comments.
*
* @param $comments
* An array of comments as returned by entity_load_multiple().
* @param $view_mode
* View mode; e.g., 'full', 'teaser', etc.
* @param $langcode
* (optional) A string indicating the language field values are to be shown
* in. If no language is provided the current content language is used.
* Defaults to NULL.
*
* @return array
* An array in the format expected by drupal_render().
*
* @see drupal_render()
*/
function comment_view_multiple($comments, $view_mode = 'full', $langcode = NULL) {
return entity_view_multiple($comments, $view_mode, $langcode);
}
/**
* 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'));
}
if (!_comment_entity_uses_integer_id($form_state->get('entity_type_id'))) {
// You cannot use comment fields on entity types with non-integer IDs.
unset($form['add']['new_storage_type']['#options'][t('General')]['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_form_FORM_ID_alter() for 'field_storage_config_edit_form'.
*/
function comment_form_field_storage_config_edit_form_alter(&$form, FormStateInterface $form_state) {
if ($form_state->getFormObject()->getEntity()->getType() == 'comment') {
// We only support posting one comment at the time so it doesn't make sense
// to let the site builder choose anything else.
$form['cardinality_container']['cardinality']['#default_value'] = 1;
$form['cardinality_container']['#access'] = FALSE;
}
}
/**
* 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::entityManager()->getDefinition($entity_type)->isSubclassOf('Drupal\Core\Entity\FieldableEntityInterface')) {
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');
$entity_query->condition('entity_id', (int) $entity->id());
$entity_query->condition('entity_type', $entity->getEntityTypeId());
$cids = $entity_query->execute();
entity_delete_multiple('comment', $cids);
\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::entityManager()->getDefinition($entity_type_id);
$entity_type_id_key = $entity_type->getKey('id');
if ($entity_type_id_key === FALSE) {
return FALSE;
}
$field_definitions = \Drupal::entityManager()->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, $langcode) {
$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::entityManager()->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 = array();
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::entityManager()->getStorage('comment')
->loadThread($node, $field_name, $mode, $comments_per_page);
if ($comments) {
$build[] = \Drupal::entityManager()->getViewBuilder('comment')->viewMultiple($comments);
}
}
}
}
return \Drupal::service('renderer')->renderPlain($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 array('comment' => \Drupal::translation()->formatPlural($comments, '1 comment', '@count comments'));
}
}
/**
* Implements hook_user_cancel().
*/
function comment_user_cancel($edit, $account, $method) {
switch ($method) {
case 'user_cancel_block_unpublish':
$comments = entity_load_multiple_by_properties('comment', array('uid' => $account->id()));
foreach ($comments as $comment) {
$comment->setPublished(CommentInterface::NOT_PUBLISHED);
$comment->save();
}
break;
case 'user_cancel_reassign':
/** @var \Drupal\comment\CommentInterface[] $comments */
$comments = entity_load_multiple_by_properties('comment', array('uid' => $account->id()));
foreach ($comments as $comment) {
$comment->setOwnerId(0);
$comment->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');
$entity_query->condition('uid', $account->id());
$cids = $entity_query->execute();
entity_delete_multiple('comment', $cids);
}
/**
* 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_render().
*/
function comment_preview(CommentInterface $comment, FormStateInterface $form_state) {
$preview_build = array();
$entity = $comment->getCommentedEntity();
if (!$form_state->getErrors()) {
// Attach the user and time information.
$author_name = $comment->getAuthorName();
if (!empty($author_name)) {
$account = user_load_by_name($author_name);
}
elseif (\Drupal::currentUser()->isAuthenticated() && empty($comment->is_anonymous)) {
$account = \Drupal::currentUser();
}
if (!empty($account) && $account->isAuthenticated()) {
$comment->setOwner($account);
$comment->setAuthorName(SafeMarkup::checkPlain($account->getUsername()));
}
elseif (empty($author_name)) {
$comment->setAuthorName(\Drupal::config('user.settings')->get('anonymous'));
}
$created_time = !is_null($comment->getCreatedTime()) ? $comment->getCreatedTime() : REQUEST_TIME;
$comment->setCreatedTime($created_time);
$comment->changed->value = REQUEST_TIME;
$comment->in_preview = TRUE;
$comment_build = comment_view($comment);
$comment_build['#weight'] = -100;
$preview_build['comment_preview'] = $comment_build;
}
if ($comment->hasParentComment()) {
$build = array();
$parent = $comment->getParentComment();
if ($parent && $parent->isPublished()) {
$build = 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 entity_view(). 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 = entity_view($entity, 'full');
}
$preview_build['comment_output_below'] = $build;
$preview_build['comment_output_below']['#weight'] = 100;
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.
*
* 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\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'];
$account = $comment->getOwner();
$username = array(
'#theme' => 'username',
'#account' => $account,
);
$variables['author'] = drupal_render($username);
$variables['author_id'] = $comment->getOwnerId();
$variables['new_indicator_timestamp'] = $comment->getChangedTime();
$variables['created'] = format_date($comment->getCreatedTime());
// Avoid calling format_date() twice on the same timestamp.
if ($comment->getChangedTime() == $comment->getCreatedTime()) {
$variables['changed'] = $variables['created'];
}
else {
$variables['changed'] = format_date($comment->getChangedTime());
}
if (theme_get_setting('features.comment_user_picture')) {
// To change user picture settings (e.g., image style), edit the 'compact'
// view mode on the User entity.
$variables['user_picture'] = user_view($account, 'compact');
}
else {
$variables['user_picture'] = array();
}
if (isset($comment->in_preview)) {
$variables['title'] = \Drupal::l($comment->getSubject(), new Url('<front>'));
$variables['permalink'] = \Drupal::l(t('Permalink'), new Url('<front>'));
}
else {
$uri = $comment->urlInfo();
$attributes = $uri->getOption('attributes') ?: array();
$attributes += array('class' => array('permalink'), 'rel' => 'bookmark');
$uri->setOption('attributes', $attributes);
$variables['title'] = \Drupal::l($comment->getSubject(), $uri);
$variables['permalink'] = \Drupal::l(t('Permalink'), $comment->permalink());
}
$variables['submitted'] = t('Submitted by @username on @datetime', array('@username' => $variables['author'], '@datetime' => $variables['created']));
if ($comment->hasParentComment()) {
// Fetch and store the parent comment information for use in templates.
$comment_parent = $comment->getParentComment();
$account_parent = $comment_parent->getOwner();
$variables['parent_comment'] = $comment_parent;
$username = array(
'#theme' => 'username',
'#account' => $account_parent,
);
$variables['parent_author'] = drupal_render($username);
$variables['parent_created'] = format_date($comment_parent->getCreatedTime());
// Avoid calling format_date() twice on the same timestamp.
if ($comment_parent->getChangedTime() == $comment_parent->getCreatedTime()) {
$variables['parent_changed'] = $variables['parent_created'];
}
else {
$variables['parent_changed'] = format_date($comment_parent->getChangedTime());
}
$permalink_uri_parent = $comment_parent->permalink();
$attributes = $permalink_uri_parent->getOption('attributes') ?: array();
$attributes += array('class' => array('permalink'), 'rel' => 'bookmark');
$permalink_uri_parent->setOption('attributes', $attributes);
$variables['parent_title'] = \Drupal::l($comment_parent->getSubject(), $permalink_uri_parent);
$variables['parent_permalink'] = \Drupal::l(t('Parent permalink'), $permalink_uri_parent);
$variables['parent'] = t('In reply to !parent_title by !parent_username',
array('!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'] = '';
}
// 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();
}
/**
* 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 (eg. RDFa) 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();
}

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,119 @@
comment.admin:
path: '/admin/content/comment'
defaults:
_title: 'Comments'
_controller: '\Drupal\comment\Controller\AdminController::adminPage'
type: 'new'
requirements:
_permission: 'administer comments'
comment.admin_approval:
path: '/admin/content/comment/approval'
defaults:
_title: 'Unapproved comments'
_controller: '\Drupal\comment\Controller\AdminController::adminPage'
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.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'
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'
entity.comment.delete_form:
path: '/comment/{comment}/delete'
defaults:
_title: 'Delete'
_entity_form: 'comment.delete'
requirements:
_entity_access: 'comment.delete'
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'
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: 'Edit'
requirements:
_entity_access: 'comment_type.update'
options:
_admin_route: TRUE

View file

@ -0,0 +1,24 @@
services:
comment.breadcrumb:
class: Drupal\comment\CommentBreadcrumbBuilder
arguments: ['@entity.manager']
tags:
- { name: breadcrumb_builder, priority: 100 }
comment.manager:
class: Drupal\comment\CommentManager
arguments: ['@entity.manager', '@entity.query', '@config.factory', '@string_translation', '@url_generator', '@module_handler', '@current_user']
comment.statistics:
class: Drupal\comment\CommentStatistics
arguments: ['@database', '@current_user', '@entity.manager', '@state']
tags:
- { name: backend_overridable }
comment.lazy_builders:
class: Drupal\comment\CommentLazyBuilders
arguments: ['@entity.manager', '@entity.form_builder', '@current_user', '@comment.manager', '@module_handler', '@renderer']
comment.link_builder:
class: Drupal\comment\CommentLinkBuilder
arguments: ['@current_user', '@comment.manager', '@module_handler', '@string_translation', '@entity.manager']

View file

@ -0,0 +1,246 @@
<?php
/**
* @file
* Builds placeholder replacement tokens for comment-related data.
*/
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Xss;
/**
* Implements hook_token_info().
*/
function comment_token_info() {
$type = array(
'name' => t('Comments'),
'description' => t('Tokens for comments posted on the site.'),
'needs-data' => 'comment',
);
// @todo Make this work per field. See https://www.drupal.org/node/2031903.
$entity['comment-count'] = array(
'name' => t("Comment count"),
'description' => t("The number of comments posted on an entity."),
);
$entity['comment-count-new'] = array(
'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'] = array(
'name' => t("Comment ID"),
'description' => t("The unique ID of the comment."),
);
$comment['hostname'] = array(
'name' => t("IP Address"),
'description' => t("The IP address of the computer the comment was posted from."),
);
$comment['mail'] = array(
'name' => t("Email address"),
'description' => t("The email address left by the comment author."),
);
$comment['homepage'] = array(
'name' => t("Home page"),
'description' => t("The home page URL left by the comment author."),
);
$comment['title'] = array(
'name' => t("Title"),
'description' => t("The title of the comment."),
);
$comment['body'] = array(
'name' => t("Content"),
'description' => t("The formatted content of the comment itself."),
);
$comment['langcode'] = array(
'name' => t('Language code'),
'description' => t('The language code of the language the comment is written in.'),
);
$comment['url'] = array(
'name' => t("URL"),
'description' => t("The URL of the comment."),
);
$comment['edit-url'] = array(
'name' => t("Edit URL"),
'description' => t("The URL of the comment's edit page."),
);
// Chained tokens for comments
$comment['created'] = array(
'name' => t("Date created"),
'description' => t("The date the comment was posted."),
'type' => 'date',
);
$comment['changed'] = array(
'name' => t("Date changed"),
'description' => t("The date the comment was most recently updated."),
'type' => 'date',
);
$comment['parent'] = array(
'name' => t("Parent"),
'description' => t("The comment's parent, if comment threading is active."),
'type' => 'comment',
);
$comment['entity'] = array(
'name' => t("Entity"),
'description' => t("The entity the comment was posted to."),
'type' => 'entity',
);
$comment['author'] = array(
'name' => t("Author"),
'description' => t("The author name of the comment."),
'type' => 'user',
);
return array(
'types' => array('comment' => $type),
'tokens' => array(
'entity' => $entity,
'comment' => $comment,
),
);
}
/**
* Implements hook_tokens().
*/
function comment_tokens($type, $tokens, array $data = array(), array $options = array()) {
$token_service = \Drupal::token();
$url_options = array('absolute' => TRUE);
if (isset($options['langcode'])) {
$url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
$langcode = $options['langcode'];
}
else {
$langcode = NULL;
}
$sanitize = !empty($options['sanitize']);
$replacements = array();
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] = $sanitize ? SafeMarkup::checkPlain($comment->getHostname()) : $comment->getHostname();
break;
case 'mail':
$mail = $comment->getAuthorEmail();
$replacements[$original] = $sanitize ? SafeMarkup::checkPlain($mail) : $mail;
break;
case 'homepage':
$replacements[$original] = $sanitize ? check_url($comment->getHomepage()) : $comment->getHomepage();
break;
case 'title':
$replacements[$original] = $sanitize ? Xss::filter($comment->getSubject()) : $comment->getSubject();
break;
case 'body':
$replacements[$original] = $sanitize ? $comment->comment_body->processed : $comment->comment_body->value;
break;
case 'langcode':
$replacements[$original] = $sanitize ? SafeMarkup::checkPlain($comment->language()->getId()) : $comment->language()->getId();
break;
// Comment related URLs.
case 'url':
$url_options['fragment'] = 'comment-' . $comment->id();
$replacements[$original] = $comment->url('canonical', $url_options);
break;
case 'edit-url':
$url_options['fragment'] = NULL;
$replacements[$original] = $comment->url('edit-form', $url_options);
break;
case 'author':
$name = $comment->getAuthorName();
$replacements[$original] = $sanitize ? Xss::filter($name) : $name;
break;
case 'parent':
if ($comment->hasParentComment()) {
$parent = $comment->getParentComment();
$replacements[$original] = $sanitize ? Xss::filter($parent->getSubject()) : $parent->getSubject();
}
break;
case 'created':
$replacements[$original] = format_date($comment->getCreatedTime(), 'medium', '', NULL, $langcode);
break;
case 'changed':
$replacements[$original] = format_date($comment->getChangedTime(), 'medium', '', NULL, $langcode);
break;
case 'entity':
$entity = $comment->getCommentedEntity();
$title = $entity->label();
$replacements[$original] = $sanitize ? Xss::filter($title) : $title;
break;
}
}
// Chained token relationships.
if ($entity_tokens = $token_service->findwithPrefix($tokens, 'entity')) {
$entity = $comment->getCommentedEntity();
$replacements += $token_service->generate($comment->getCommentedEntityTypeId(), $entity_tokens, array($comment->getCommentedEntityTypeId() => $entity), $options);
}
if ($date_tokens = $token_service->findwithPrefix($tokens, 'created')) {
$replacements += $token_service->generate('date', $date_tokens, array('date' => $comment->getCreatedTime()), $options);
}
if ($date_tokens = $token_service->findwithPrefix($tokens, 'changed')) {
$replacements += $token_service->generate('date', $date_tokens, array('date' => $comment->getChangedTime()), $options);
}
if (($parent_tokens = $token_service->findwithPrefix($tokens, 'parent')) && $parent = $comment->getParentComment()) {
$replacements += $token_service->generate('comment', $parent_tokens, array('comment' => $parent), $options);
}
if (($author_tokens = $token_service->findwithPrefix($tokens, 'author')) && $account = $comment->getOwner()) {
$replacements += $token_service->generate('user', $author_tokens, array('user' => $account), $options);
}
}
elseif ($type == 'entity' & !empty($data['entity'])) {
/** @var $entity \Drupal\Core\Entity\FieldableEntityInterface */
$entity = $data['entity'];
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,93 @@
<?php
/**
* @file
* Provide views data for comment.module.
*/
/**
* 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'] = array(
'title' => t('New comments'),
'help' => t('The number of new comments on the node.'),
'field' => array(
'id' => 'node_new_comments',
'no group by' => TRUE,
),
);
// Provide a integration for each entity type except comment.
foreach (\Drupal::entityManager()->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type_id == 'comment' || !$entity_type->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface') || !$entity_type->getBaseTable()) {
continue;
}
$fields = \Drupal::service('comment.manager')->getFields($entity_type_id);
$base_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable();
$args = array('@entity_type' => $entity_type_id);
if ($fields) {
$data[$base_table]['comments_link'] = array(
'field' => array(
'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'] = array(
'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' => array(
'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' => array(
'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'] = array(
'title' => t('Comments of the @entity_type using field: @field_name', $args + array('@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' => array(
'group' => t('Comment'),
'label' => t('Comments'),
'base' => 'comment_field_data',
'base field' => 'entity_id',
'relationship field' => $entity_type->getKey('id'),
'id' => 'standard',
'extra' => array(
array(
'field' => 'entity_type',
'value' => $entity_type_id,
),
array(
'field' => 'field_name',
'value' => $field_name,
),
),
),
);
}
}
}
}

View file

@ -0,0 +1,8 @@
id: comment.full
label: 'Full comment'
status: false
cache: true
targetEntityType: comment
dependencies:
module:
- comment

View file

@ -0,0 +1,17 @@
langcode: en
status: true
dependencies:
module:
- comment
- text
id: comment.comment_body
field_name: comment_body
entity_type: comment
type: text_long
settings: { }
module: text
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: true

View file

@ -0,0 +1,9 @@
id: comment_publish_action
label: 'Publish comment'
status: true
langcode: en
type: comment
plugin: comment_publish_action
dependencies:
module:
- comment

View file

@ -0,0 +1,9 @@
id: comment_save_action
label: 'Save comment'
status: true
langcode: en
type: comment
plugin: comment_save_action
dependencies:
module:
- comment

View file

@ -0,0 +1,9 @@
id: comment_unpublish_action
label: 'Unpublish comment'
status: true
langcode: en
type: comment
plugin: comment_unpublish_action
dependencies:
module:
- comment

View file

@ -0,0 +1,245 @@
langcode: en
status: true
dependencies:
module:
- comment
- user
id: comments_recent
label: 'Recent comments'
module: views
description: 'Recent comments.'
tag: default
base_table: comment_field_data
base_field: cid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access comments'
cache:
type: tag
query:
type: views_query
exposed_form:
type: basic
pager:
type: some
options:
items_per_page: 10
offset: 0
style:
type: html_list
options:
grouping: { }
row_class: ''
default_row_class: true
type: ul
wrapper_class: item-list
class: ''
row:
type: fields
options:
default_field_elements: true
inline:
subject: subject
changed: changed
separator: ' '
hide_empty: false
relationships:
node:
field: node
id: node
table: comment_field_data
required: true
plugin_id: standard
fields:
subject:
id: subject
table: comment_field_data
field: subject
relationship: none
type: string
settings:
link_to_entity: true
plugin_id: field
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: false
ellipsis: false
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
entity_type: comment
entity_field: subject
changed:
id: changed
table: comment_field_data
field: changed
relationship: none
plugin_id: date
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
date_format: 'time ago'
custom_date_format: ''
timezone: ''
entity_type: comment
entity_field: changed
filters:
status:
value: true
table: comment_field_data
field: status
id: status
plugin_id: boolean
expose:
operator: ''
group: 1
entity_type: comment
entity_field: status
status_node:
value: true
table: node_field_data
field: status
relationship: node
id: status_node
plugin_id: boolean
expose:
operator: ''
group: 1
entity_type: node
entity_field: status
sorts:
created:
id: created
table: comment_field_data
field: created
relationship: none
group_type: group
admin_label: ''
order: DESC
exposed: false
expose:
label: ''
plugin_id: date
entity_type: comment
entity_field: created
cid:
id: cid
table: comment_field_data
field: cid
relationship: none
group_type: group
admin_label: ''
order: DESC
exposed: false
plugin_id: field
entity_type: comment
entity_field: cid
title: 'Recent comments'
empty:
area_text_custom:
id: area_text_custom
table: views
field: area_text_custom
relationship: none
group_type: group
admin_label: ''
label: ''
empty: true
content: 'No comments available.'
tokenize: false
plugin_id: text_custom
block_1:
display_plugin: block
id: block_1
display_title: Block
position: 1
display_options:
block_description: 'Recent comments'
block_category: 'Lists (Views)'
allow:
items_per_page: true

View file

@ -0,0 +1,104 @@
# Schema for the configuration files of the Comment module.
field.formatter.settings.comment_default:
type: mapping
label: 'Comment display format settings'
mapping:
pager_id:
type: integer
label: 'Pager ID'
field.widget.settings.comment_default:
type: mapping
label: 'Comment display format settings'
action.configuration.comment_publish_action:
type: action_configuration_default
label: 'Publish comment configuration'
action.configuration.comment_save_action:
type: action_configuration_default
label: 'Save comment configuration'
action.configuration.comment_unpublish_by_keyword_action:
type: mapping
label: 'Unpublish comment containing keyword(s) configuration'
mapping:
keywords:
type: sequence
label: 'Keywords'
sequence:
type: string
label: 'Keyword'
action.configuration.comment_unpublish_action:
type: action_configuration_default
label: 'Unpublish comment configuration'
comment.type.*:
type: config_entity
label: 'Comment type settings'
mapping:
id:
type: string
label: 'ID'
label:
type: label
label: 'Label'
target_entity_type_id:
type: string
label: 'Target Entity Type ID'
description:
type: text
label: 'Description'
field.storage_settings.comment:
type: mapping
label: 'Comment settings'
mapping:
comment_type:
label: 'Comment type'
type: string
field.value.comment:
type: mapping
label: 'Default value'
mapping:
status:
type: integer
label: 'Comment status'
cid:
type: integer
label: 'Last comment ID'
last_comment_timestamp:
type: integer
label: 'Last comment timestamp'
last_comment_name:
type: integer
label: 'Last comment name'
last_comment_uid:
type: integer
label: 'Last comment user ID'
comment_count:
type: integer
label: 'Number of comments'
field.field_settings.comment:
type: mapping
label: 'Comment settings'
mapping:
default_mode:
type: integer
label: 'Threading'
per_page:
type: integer
label: 'Comments per page'
anonymous:
type: integer
label: 'Mode'
form_location:
type: boolean
label: ' Allow comment title'
preview:
type: integer
label: 'Preview comment'

View file

@ -0,0 +1,77 @@
# Schema for the views plugins of the Comment module.
views.argument.argument_comment_user_uid:
type: views_argument
label: 'Commented user ID'
views.field.comment_depth:
type: views_field
label: 'Comment depth'
views.field.comment_entity_link:
type: views_field
label: 'Comment link'
mapping:
teaser:
type: boolean
label: 'Show teaser-style link'
views.field.comment_last_timestamp:
type: views.field.date
label: 'Last comment date'
views.field.comment_link_approve:
type: views.field.comment_link
label: 'Comment approve link'
views.field.comment_link_reply:
type: views.field.comment_link
label: 'Comment reply link'
views.field.node_new_comments:
type: views.field.numeric
label: 'Number of new comments'
mapping:
link_to_comment:
type: boolean
label: 'Link this field to new comments'
views.field.comment_ces_last_comment_name:
type: views_field
label: 'Name of last comment poster'
views.field.comment_ces_last_updated:
type: views_field
label: 'Newer of last comment / node updated'
views.filter.node_comment:
type: views.filter.in_operator
label: 'Comment node status'
views.filter.comment_ces_last_updated:
type: views.filter.date
label: 'Newer of last comment / node updated'
views.filter.comment_user_uid:
type: views_filter
label: 'Node user posted comment'
views.row.comment_rss:
type: views_row
label: 'Comment'
mapping:
view_mode:
type: string
label: 'Display type'
views.sort.comment_ces_last_comment_name:
type: views_sort
label: 'Last comment name'
views.sort.comment_ces_last_updated:
type: views.sort.date
label: 'Newer of last comment / entity updated'
views.sort.comment_thread:
type: views_sort
label: 'Thread'

View file

@ -0,0 +1,26 @@
/**
* @file
* Attaches behaviors for the Comment module's "by-viewer" class.
*/
(function ($, Drupal, drupalSettings) {
"use strict";
/**
* Add 'by-viewer' class to comments written by the current user.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.commentByViewer = {
attach: function (context) {
var 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,87 @@
/**
* @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) {
"use strict";
/**
* Render "new" comment indicators wherever necessary.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.commentNewIndicator = {
attach: function (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.
var nodeIDs = [];
var $placeholders = $(context)
.find('[data-comment-timestamp]')
.once('history')
.filter(function () {
var $placeholder = $(this);
var commentTimestamp = parseInt($placeholder.attr('data-comment-timestamp'), 10);
var nodeID = $placeholder.closest('[data-history-node-id]').attr('data-history-node-id');
if (Drupal.history.needsServerCheck(nodeID, commentTimestamp)) {
nodeIDs.push(nodeID);
return true;
}
else {
return false;
}
});
if ($placeholders.length === 0) {
return;
}
// Fetch the node read timestamps from the server.
Drupal.history.fetchTimestamps(nodeIDs, function () {
processCommentNewIndicators($placeholders);
});
}
};
function processCommentNewIndicators($placeholders) {
var isFirstNewComment = true;
var newCommentString = Drupal.t('new');
var $placeholder;
$placeholders.each(function (index, placeholder) {
$placeholder = $(placeholder);
var timestamp = parseInt($placeholder.attr('data-comment-timestamp'), 10);
var $node = $placeholder.closest('[data-history-node-id]');
var nodeID = $node.attr('data-history-node-id');
var lastViewTimestamp = Drupal.history.getLastRead(nodeID);
if (timestamp > lastViewTimestamp) {
// Turn the placeholder into an actual "new" indicator.
var $comment = $(placeholder)
.removeClass('hidden')
.text(newCommentString)
.closest('.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;
$comment.prev().before('<a id="new" />');
// If the URL points to the first new comment, then scroll to that
// comment.
if (window.location.hash === '#new') {
window.scrollTo(0, $comment.offset().top - Drupal.displace.offsets.top);
}
}
}
});
}
})(jQuery, Drupal, window);

View file

@ -0,0 +1,136 @@
/**
* @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) {
"use strict";
/**
* Render "X new comments" links wherever necessary.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.nodeNewCommentsLink = {
attach: function (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.
var nodeIDs = [];
var $placeholders = $(context)
.find('[data-history-node-last-comment-timestamp]')
.once('history')
.filter(function () {
var $placeholder = $(this);
var lastCommentTimestamp = parseInt($placeholder.attr('data-history-node-last-comment-timestamp'), 10);
var 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;
}
else {
// 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, function () {
processNodeNewCommentLinks($placeholders);
});
}
};
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();
}
function remove($placeholder) {
hide($placeholder).remove();
}
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();
}
function processNodeNewCommentLinks($placeholders) {
// Figure out which placeholders need the "x new comments" links.
var $placeholdersToUpdate = {};
var fieldName = 'comment';
var $placeholder;
$placeholders.each(function (index, placeholder) {
$placeholder = $(placeholder);
var timestamp = parseInt($placeholder.attr('data-history-node-last-comment-timestamp'), 10);
fieldName = $placeholder.attr('data-history-node-field-name');
var nodeID = $placeholder.closest('[data-history-node-id]').attr('data-history-node-id');
var 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.
var nodeIDs = Object.keys($placeholdersToUpdate);
if (nodeIDs.length === 0) {
return;
}
// Render the "X new comments" links. Either use the data embedded in the page
// or perform an AJAX request to retrieve the same data.
function render(results) {
for (var nodeID in results) {
if (results.hasOwnProperty(nodeID) && $placeholdersToUpdate.hasOwnProperty(nodeID)) {
$placeholdersToUpdate[nodeID]
.attr('href', results[nodeID].first_new_comment_link)
.text(Drupal.formatPlural(results[nodeID].new_comment_count, '1 new comment', '@count new comments'))
.removeClass('hidden');
show($placeholdersToUpdate[nodeID]);
}
}
}
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
});
}
}
})(jQuery, Drupal);

View file

@ -0,0 +1,138 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentAccessControlHandler.
*/
namespace Drupal\comment;
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, $langcode, 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()
->cacheUntilEntityChanges($entity);
}
if ($comment_admin) {
$access = AccessResult::allowed()->cachePerPermissions();
return ($operation != 'view') ? $access : $access->andIf($entity->getCommentedEntity()->access($operation, $account, TRUE));
}
switch ($operation) {
case 'view':
return AccessResult::allowedIf($account->hasPermission('access comments') && $entity->isPublished())->cachePerPermissions()->cacheUntilEntityChanges($entity)
->andIf($entity->getCommentedEntity()->access($operation, $account, TRUE));
case 'update':
return AccessResult::allowedIf($account->id() && $account->id() == $entity->getOwnerId() && $entity->isPublished() && $account->hasPermission('edit own comments'))->cachePerPermissions()->cachePerUser()->cacheUntilEntityChanges($entity);
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 = array(
'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 = array(
'hostname',
'uuid',
'cid',
'thread',
'comment_type',
'pid',
'entity_id',
'entity_type',
'field_name',
);
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();
}
/** @var \Drupal\comment\CommentInterface $entity */
$entity = $items->getEntity();
$commented_entity = $entity->getCommentedEntity();
$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 != COMMENT_ANONYMOUS_MAYNOT_CONTACT && $account->hasPermission('post comments'))
->cachePerPermissions()
->cacheUntilEntityChanges($entity)
->cacheUntilEntityChanges($field_definition->getConfig($commented_entity->bundle()))
->cacheUntilEntityChanges($commented_entity);
return $admin_access->orIf($anonymous_access);
}
}
if ($operation == 'view') {
$entity = $items ? $items->getEntity() : NULL;
// Admins can view any fields except hostname, other users need both the
// "access comments" permission and for the comment to be published. The
// mail field is hidden from non-admins.
$admin_access = AccessResult::allowedIf($account->hasPermission('administer comments') && $field_definition->getName() != 'hostname')
->cachePerPermissions();
$anonymous_access = AccessResult::allowedIf($account->hasPermission('access comments') && (!$entity || $entity->isPublished()) && !in_array($field_definition->getName(), array('mail', 'hostname'), TRUE))
->cachePerPermissions();
if ($entity) {
$anonymous_access->cacheUntilEntityChanges($entity);
}
return $admin_access->orIf($anonymous_access);
}
return parent::checkFieldAccess($operation, $field_definition, $account, $items);
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentBreadcrumbBuilder.
*/
namespace Drupal\comment;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Entity\EntityManagerInterface;
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 comment storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* Constructs the CommentBreadcrumbBuilder.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entity_manager) {
$this->storage = $entity_manager->getStorage('comment');
}
/**
* {@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 = [Link::createFromRoute($this->t('Home'), '<front>')];
$entity = $route_match->getParameter('entity');
$breadcrumb[] = new Link($entity->label(), $entity->urlInfo());
if (($pid = $route_match->getParameter('pid')) && ($comment = $this->storage->load($pid))) {
/** @var \Drupal\comment\CommentInterface $comment */
// Display link to parent comment.
// @todo Clean-up permalink in https://www.drupal.org/node/2198041
$breadcrumb[] = new Link($comment->getSubject(), $comment->urlInfo());
}
return $breadcrumb;
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentFieldItemList.
*/
namespace Drupal\comment;
use Drupal\Core\Field\FieldItemList;
/**
* Defines a 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}
*/
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);
}
}

View file

@ -0,0 +1,400 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentForm.
*/
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base for controller for comment forms.
*/
class CommentForm extends ContentEntityForm {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager'),
$container->get('current_user'),
$container->get('renderer')
);
}
/**
* Constructs a new CommentForm.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(EntityManagerInterface $entity_manager, AccountInterface $current_user, RendererInterface $renderer) {
parent::__construct($entity_manager);
$this->currentUser = $current_user;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
protected function init(FormStateInterface $form_state) {
$comment = $this->entity;
// Make the comment inherit the current content language unless specifically
// set.
if ($comment->isNew()) {
$language_content = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT);
$comment->langcode->value = $language_content->getId();
}
parent::init($form_state);
}
/**
* Overrides Drupal\Core\Entity\EntityForm::form().
*/
public function form(array $form, FormStateInterface $form_state) {
/** @var \Drupal\comment\CommentInterface $comment */
$comment = $this->entity;
$entity = $this->entityManager->getStorage($comment->getCommentedEntityTypeId())->load($comment->getCommentedEntityId());
$field_name = $comment->getFieldName();
$field_definition = $this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle())[$comment->getFieldName()];
$config = $this->config('user.settings');
// Use #comment-form as unique jump target, regardless of entity type.
$form['#id'] = Html::getUniqueId('comment_form');
$form['#theme'] = array('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 != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
$form['#attached']['library'][] = 'core/drupal.form';
$form['#attributes']['data-user-info-from-browser'] = TRUE;
}
// Vary per role, because we check a permission above and attach an asset
// library only for authenticated users.
$form['#cache']['contexts'][] = 'user.roles';
// If not replying to a comment, use our dedicated page callback for new
// Comments on entities.
if (!$comment->id() && !$comment->hasParentComment()) {
$form['#action'] = $this->url('comment.reply', array('entity_type' => $entity->getEntityTypeId(), 'entity' => $entity->id(), 'field_name' => $field_name));
}
$comment_preview = $form_state->get('comment_preview');
if (isset($comment_preview)) {
$form += $comment_preview;
}
$form['author'] = array();
// Display author information in a details element for comment moderators.
if ($is_admin) {
$form['author'] += array(
'#type' => 'details',
'#title' => $this->t('Administration'),
);
}
// Prepare default values for form elements.
if ($is_admin) {
$author = $comment->getAuthorName();
$status = $comment->getStatus();
if (empty($comment_preview)) {
$form['#title'] = $this->t('Edit comment %title', array(
'%title' => $comment->getSubject(),
));
}
}
else {
if ($this->currentUser->isAuthenticated()) {
$author = $this->currentUser->getUsername();
}
else {
$author = ($comment->getAuthorName() ? $comment->getAuthorName() : '');
}
$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());
}
// Add the author name field depending on the current user.
$form['author']['name'] = array(
'#type' => 'textfield',
'#title' => $this->t('Your name'),
'#default_value' => $author,
'#required' => ($this->currentUser->isAnonymous() && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
'#maxlength' => 60,
'#size' => 30,
);
if ($is_admin) {
$form['author']['name']['#type'] = 'entity_autocomplete';
$form['author']['name']['#target_type'] = 'user';
$form['author']['name']['#selection_settings'] = ['include_anonymous' => FALSE];
$form['author']['name']['#process_default_value'] = FALSE;
// The user name is validated and processed in static::buildEntity() and
// static::validate().
$form['author']['name']['#element_validate'] = array();
$form['author']['name']['#title'] = $this->t('Authored by');
$form['author']['name']['#description'] = $this->t('Leave blank for %anonymous.', array('%anonymous' => $config->get('anonymous')));
}
elseif ($this->currentUser->isAuthenticated()) {
$form['author']['name']['#type'] = 'item';
$form['author']['name']['#value'] = $form['author']['name']['#default_value'];
$form['author']['name']['#theme'] = 'username';
$form['author']['name']['#account'] = $this->currentUser;
}
elseif($this->currentUser->isAnonymous()) {
$form['author']['name']['#attributes']['data-drupal-default-value'] = $config->get('anonymous');
}
// Add author email and homepage fields depending on the current user.
$form['author']['mail'] = array(
'#type' => 'email',
'#title' => $this->t('Email'),
'#default_value' => $comment->getAuthorEmail(),
'#required' => ($this->currentUser->isAnonymous() && $anonymous_contact == COMMENT_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 != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
);
$form['author']['homepage'] = array(
'#type' => 'url',
'#title' => $this->t('Homepage'),
'#default_value' => $comment->getHomepage(),
'#maxlength' => 255,
'#size' => 30,
'#access' => $is_admin || ($this->currentUser->isAnonymous() && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
);
// Add administrative comment publishing options.
$form['author']['date'] = array(
'#type' => 'datetime',
'#title' => $this->t('Authored on'),
'#default_value' => $date,
'#size' => 20,
'#access' => $is_admin,
);
$form['author']['status'] = array(
'#type' => 'radios',
'#title' => $this->t('Status'),
'#default_value' => $status,
'#options' => array(
CommentInterface::PUBLISHED => $this->t('Published'),
CommentInterface::NOT_PUBLISHED => $this->t('Not published'),
),
'#access' => $is_admin,
);
$this->renderer->addCacheableDependency($form, $config);
// The form depends on the field definition.
$this->renderer->addCacheableDependency($form, $field_definition->getConfig($entity->bundle()));
return parent::form($form, $form_state, $comment);
}
/**
* Overrides Drupal\Core\Entity\EntityForm::actions().
*/
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->entityManager->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'] = array(
'#type' => 'submit',
'#value' => $this->t('Preview'),
'#access' => $preview_mode != DRUPAL_DISABLED,
'#validate' => array('::validate'),
'#submit' => array('::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(REQUEST_TIME);
}
$author_name = $form_state->getValue('name');
if (!$this->currentUser->isAnonymous()) {
// Assign the owner based on the given user name - none means anonymous.
$accounts = $this->entityManager->getStorage('user')
->loadByProperties(array('name' => $author_name));
$account = reset($accounts);
$uid = $account ? $account->id() : 0;
$comment->setOwnerId($uid);
}
// If the comment was posted by an anonymous user and no author name was
// required, use "Anonymous" by default.
if ($comment->getOwnerId() === 0 && (!isset($author_name) || $author_name === '')) {
$comment->setAuthorName($this->config('user.settings')->get('anonymous'));
}
// Validate the comment's subject. If not specified, extract from comment
// body.
if (trim($comment->getSubject()) == '') {
if ($comment->hasField('comment_body')) {
// 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 ($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 $form
* An associative array containing the structure of the form.
* @param $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();
}
/**
* Overrides Drupal\Core\Entity\EntityForm::save().
*/
public function save(array $form, FormStateInterface $form_state) {
$comment = $this->entity;
$entity = $comment->getCommentedEntity();
$field_name = $comment->getFieldName();
$uri = $entity->urlInfo();
$logger = $this->logger('content');
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->notice('Comment posted: %subject.', array(
'%subject' => $comment->getSubject(),
'link' => $this->l(t('View'), $comment->urlInfo()->setOption('fragment', 'comment-' . $comment->id()))
));
// Explain the approval queue if necessary.
if (!$comment->isPublished()) {
if (!$this->currentUser->hasPermission('administer comments')) {
drupal_set_message($this->t('Your comment has been queued for review by site administrators and will be published after approval.'));
}
}
else {
drupal_set_message($this->t('Your comment has been posted.'));
}
$query = array();
// Find the current display page for this comment.
$field_definition = $this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle())[$field_name];
$page = $this->entityManager->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.', array('%subject' => $comment->getSubject()));
drupal_set_message($this->t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->getSubject())), 'error');
// Redirect the user to the entity they are commenting on.
}
$form_state->setRedirectUrl($uri);
}
}

View file

@ -0,0 +1,260 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentInterface.
*/
namespace Drupal\comment;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\user\EntityOwnerInterface;
use Drupal\Core\Entity\EntityChangedInterface;
/**
* Provides an interface defining a comment entity.
*/
interface CommentInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface {
/**
* Comment is awaiting approval.
*/
const NOT_PUBLISHED = 0;
/**
* Comment is published.
*/
const PUBLISHED = 1;
/**
* 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
* The entity on which the comment is attached.
*/
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);
/**
* Checks if the comment is published.
*
* @return bool
* TRUE if the comment is published.
*/
public function isPublished();
/**
* Returns the comment's status.
*
* @return int
* One of CommentInterface::PUBLISHED or CommentInterface::NOT_PUBLISHED
*/
public function getStatus();
/**
* Sets the published status of the comment entity.
*
* @param bool $status
* Set to TRUE to publish the comment, FALSE to unpublish.
*
* @return \Drupal\comment\CommentInterface
* The class instance that this method is called on.
*/
public function setPublished($status);
/**
* Returns the alphadecimal representation of the comment's place in a thread.
*
* @return string
* The alphadecimal representation of the comment's place in a thread.
*/
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,232 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentLazyBuilders.
*/
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Entity\EntityFormBuilderInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\Core\Render\Renderer;
/**
* Defines a service for comment #lazy_builder callbacks.
*/
class CommentLazyBuilders {
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* 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\Renderer
*/
protected $renderer;
/**
* Constructs a new CommentLazyBuilders object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity 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\Renderer $renderer
* The renderer service.
*/
public function __construct(EntityManagerInterface $entity_manager, EntityFormBuilderInterface $entity_form_builder, AccountInterface $current_user, CommentManagerInterface $comment_manager, ModuleHandlerInterface $module_handler, Renderer $renderer) {
$this->entityManager = $entity_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 = array(
'entity_type' => $commented_entity_type_id,
'entity_id' => $commented_entity_id,
'field_name' => $field_name,
'comment_type' => $comment_type_id,
'pid' => NULL,
);
$comment = $this->entityManager->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 = array(
'#theme' => 'links__comment',
'#pre_render' => array('drupal_pre_render_links'),
'#attributes' => array('class' => array('links', 'inline')),
);
if (!$is_in_preview) {
/** @var \Drupal\comment\CommentInterface $entity */
$entity = $this->entityManager->getStorage('comment')->load($comment_entity_id);
$commented_entity = $entity->getCommentedEntity();
$links['comment'] = $this->buildLinks($entity, $commented_entity);
// Allow other modules to alter the comment links.
$hook_context = array(
'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 drupal_pre_render_links().
*/
protected function buildLinks(CommentInterface $entity, EntityInterface $commented_entity) {
$links = array();
$status = $commented_entity->get($entity->getFieldName())->status;
if ($status == CommentItemInterface::OPEN) {
if ($entity->access('delete')) {
$links['comment-delete'] = array(
'title' => t('Delete'),
'url' => $entity->urlInfo('delete-form'),
);
}
if ($entity->access('update')) {
$links['comment-edit'] = array(
'title' => t('Edit'),
'url' => $entity->urlInfo('edit-form'),
);
}
if ($entity->access('create')) {
$links['comment-reply'] = array(
'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'] = array(
'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'] = array(
'title' => t('Translate'),
'url' => $entity->urlInfo('drupal:content-translation-overview'),
);
}
return array(
'#theme' => 'links__comment__comment',
// The "entity" property is specified to be present, so no need to check.
'#links' => $links,
'#attributes' => array('class' => array('links', 'inline')),
);
}
/**
* Wraps content_translation_translate_access.
*/
protected function access(EntityInterface $entity) {
return content_translation_translate_access($entity);
}
}

View file

@ -0,0 +1,229 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentLinkBuilder.
*/
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityManagerInterface;
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 'login 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 manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* 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\EntityManagerInterface $entity_manager
* The entity manager service.
*/
public function __construct(AccountInterface $current_user, CommentManagerInterface $comment_manager, ModuleHandlerInterface $module_handler, TranslationInterface $string_translation, EntityManagerInterface $entity_manager) {
$this->currentUser = $current_user;
$this->commentManager = $comment_manager;
$this->moduleHandler = $module_handler;
$this->stringTranslation = $string_translation;
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
public function buildCommentedEntityLinks(FieldableEntityInterface $entity, array &$context) {
$entity_links = array();
$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 array();
}
$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 = array();
$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'] = array(
'title' => $this->formatPlural($entity->get($field_name)->comment_count, '1 comment', '@count comments'),
'attributes' => array('title' => $this->t('Jump to the first comment.')),
'fragment' => 'comments',
'url' => $entity->urlInfo(),
);
if ($this->moduleHandler->moduleExists('history')) {
$links['comment-new-comments'] = array(
'title' => '',
'url' => Url::fromRoute('<current>'),
'attributes' => array(
'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'] = array(
'title' => $this->t('Add new comment'),
'language' => $entity->language(),
'attributes' => array('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->urlInfo()];
}
}
elseif ($this->currentUser->isAnonymous()) {
$links['comment-forbidden'] = array(
'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'] = array(
'title' => $this->t('Add new comment'),
'attributes' => array('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->urlInfo();
}
}
}
elseif ($this->currentUser->isAnonymous()) {
$links['comment-forbidden'] = array(
'title' => $this->commentManager->forbiddenMessage($entity, $field_name),
);
}
}
}
}
if (!empty($links)) {
$entity_links['comment__' . $field_name] = array(
'#theme' => 'links__entity__comment__' . $field_name,
'#links' => $links,
'#attributes' => array('class' => array('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()] = (int) history_read($entity->id());
$new_comments = $this->commentManager->getCountNewComments($entity);
if ($new_comments > 0) {
$page_number = $this->entityManager
->getStorage('comment')
->getNewCommentPageNumber($entity->{$field_name}->comment_count, $new_comments, $entity);
$query = $page_number ? ['page' => $page_number] : NULL;
$value = [
'new_comment_count' => (int) $new_comments,
'first_new_comment_link' => $entity->url('canonical', [
'query' => $query,
'fragment' => 'new',
]),
];
$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,32 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentLinkBuilderInterface.
*/
namespace Drupal\comment;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* Defines an interface for building comment links on a commented entity.
*
* Comment links include 'login 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,235 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentManager.
*/
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Routing\UrlGeneratorTrait;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\user\RoleInterface;
/**
* Comment manager contains common functions to manage comment fields.
*/
class CommentManager implements CommentManagerInterface {
use StringTranslationTrait;
use UrlGeneratorTrait;
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The entity query factory.
*
* @var \Drupal\Core\Entity\Query\QueryFactory
*/
protected $queryFactory;
/**
* 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\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
* The entity query factory.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The url generator service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(EntityManagerInterface $entity_manager, QueryFactory $query_factory, ConfigFactoryInterface $config_factory, TranslationInterface $string_translation, UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, AccountInterface $current_user) {
$this->entityManager = $entity_manager;
$this->queryFactory = $query_factory;
$this->userConfig = $config_factory->get('user.settings');
$this->stringTranslation = $string_translation;
$this->urlGenerator = $url_generator;
$this->moduleHandler = $module_handler;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public function getFields($entity_type_id) {
$entity_type = $this->entityManager->getDefinition($entity_type_id);
if (!$entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
return array();
}
$map = $this->entityManager->getFieldMapByFieldType('comment');
return isset($map[$entity_type_id]) ? $map[$entity_type_id] : array();
}
/**
* {@inheritdoc}
*/
public function addBodyField($comment_type_id) {
if (!FieldConfig::loadByName('comment', $comment_type_id, 'comment_body')) {
// Attaches the body field by default.
$field = $this->entityManager->getStorage('field_config')->create(array(
'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.
entity_get_form_display('comment', $comment_type_id, 'default')
->setComponent('comment_body', array(
'type' => 'text_textarea',
))
->save();
// Assign display settings for the 'default' view mode.
entity_get_display('comment', $comment_type_id, 'default')
->setComponent('comment_body', array(
'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->entityManager
->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 = array('destination' => $this->url('comment.reply', $comment_reply_parameters, array('fragment' => 'comment-form')));
}
else {
$destination = array('destination' => $entity->url('canonical', array('fragment' => 'comment-form')));
}
if ($this->userConfig->get('register') != USER_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', array(
'@login' => $this->urlGenerator->generateFromRoute('user.login', array(), array('query' => $destination)),
'@register' => $this->urlGenerator->generateFromRoute('user.register', array(), array('query' => $destination)),
));
}
else {
// Only admins can add new users, no public registration.
return $this->t('<a href="@login">Log in</a> to post comments', array(
'@login' => $this->urlGenerator->generateFromRoute('user.login', array(), array('query' => $destination)),
));
}
}
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 once https://www.drupal.org/node/1029708 lands.
$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->queryFactory->get('comment')
->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,87 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentManagerInterface.
*/
namespace Drupal\comment;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
/**
* 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.
*
* @see \Drupal\Core\Entity\EntityManagerInterface::getFieldMap()
*/
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,265 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentStatistics.
*/
namespace Drupal\comment;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
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 current logged in user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* 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\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(Connection $database, AccountInterface $current_user, EntityManagerInterface $entity_manager, StateInterface $state) {
$this->database = $database;
$this->currentUser = $current_user;
$this->entityManager = $entity_manager;
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public function read($entities, $entity_type, $accurate = TRUE) {
$options = $accurate ? array() : array('target' => 'replica');
$stats = $this->database->select('comment_entity_statistics', 'ces', $options)
->fields('ces')
->condition('ces.entity_id', array_keys($entities), 'IN')
->condition('ces.entity_type', $entity_type)
->execute();
$statistics_records = array();
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(array(
'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 = REQUEST_TIME;
// @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(array(
'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', array(':entity_type' => $entity_type))->fetchField();
}
/**
* {@inheritdoc}
*/
public function getRankingInfo() {
return array(
'comments' => array(
'title' => t('Number of comments'),
'join' => array(
'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' => array(':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', array('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(array(
'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(array(
'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(array(
'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() : REQUEST_TIME,
'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->entityManager->getStorage($comment->getCommentedEntityTypeId())->resetCache(array($comment->getCommentedEntityId()));
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentStatisticsInterface.
*/
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,340 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentStorage.
*/
namespace Drupal\comment;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
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 controller 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\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(EntityTypeInterface $entity_info, Connection $database, EntityManagerInterface $entity_manager, AccountInterface $current_user, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
parent::__construct($entity_info, $database, $entity_manager, $cache, $language_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.manager'),
$container->get('current_user'),
$container->get('cache.entity'),
$container->get('language_manager')
);
}
/**
* {@inheritdoc}
*/
public function getMaxThread(CommentInterface $comment) {
$query = $this->database->select('comment_field_data', '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('comment_field_data', '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.
$query = $this->database->select('comment_field_data', 'c1');
$query->innerJoin('comment_field_data', '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 = 'comment') {
$field = $entity->getFieldDefinition($field_name);
$comments_per_page = $field->getSetting('per_page');
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('comment_field_data', 'comment')
->fields('comment', array('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', array('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 {comment_field_data} 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', array(
':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('comment_field_data', 'c')
->fields('c', array('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) {
$query = $this->database->select('comment_field_data', '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('Drupal\Core\Database\Query\PagerSelectExtender')
->limit($comments_per_page);
if ($pager_id) {
$query->element($pager_id);
}
$count_query = $this->database->select('comment_field_data', '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 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 = array();
if ($cids) {
$comments = $this->loadMultiple($cids);
}
return $comments;
}
/**
* {@inheritdoc}
*/
public function getUnapprovedCount() {
return $this->database->select('comment_field_data', 'c')
->condition('status', CommentInterface::NOT_PUBLISHED, '=')
->condition('default_langcode', 1)
->countQuery()
->execute()
->fetchField();
}
}

View file

@ -0,0 +1,118 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentStorageInterface.
*/
namespace Drupal\comment;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* Defines an interface for comment entity storage classes.
*/
interface CommentStorageInterface extends EntityStorageInterface {
/**
* Gets the maximum encoded thread value for the top level comments.
*
* @param \Drupal\comment\CommentInterface $comment
* A comment entity.
*
* @return string
* The maximum encoded thread value among the top level comments of the
* node $comment belongs to.
*/
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
* The maximum encoded thread value among all replies of $comment.
*/
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 = 'comment');
/**
* 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,78 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentStorageSchema.
*/
namespace Drupal\comment;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Defines the comment schema handler.
*/
class CommentStorageSchema extends SqlContentEntityStorageSchema {
/**
* {@inheritdoc}
*/
protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
$schema = parent::getEntitySchema($entity_type, $reset);
$schema['comment_field_data']['indexes'] += array(
'comment__status_pid' => array('pid', 'status'),
'comment__num_new' => array(
'entity_id',
'entity_type',
'comment_type',
'status',
'created',
'cid',
'thread',
),
'comment__entity_langcode' => array(
'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 'created':
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
break;
case 'uid':
$this->addSharedTableFieldForeignKey($storage_definition, $schema, 'users', 'uid');
}
}
return $schema;
}
}

View file

@ -0,0 +1,54 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentTranslationHandler.
*/
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', array('@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,179 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentTypeForm.
*/
namespace Drupal\comment;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\language\Entity\ContentLanguageSettings;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base form controller for category edit forms.
*/
class CommentTypeForm extends EntityForm {
/**
* Entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* 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.manager'),
$container->get('logger.factory')->get('comment'),
$container->get('comment.manager')
);
}
/**
* Constructs a CommentTypeFormController
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\comment\CommentManagerInterface $comment_manager
* The comment manager.
*/
public function __construct(EntityManagerInterface $entity_manager, LoggerInterface $logger, CommentManagerInterface $comment_manager) {
$this->entityManager = $entity_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;
$form['label'] = array(
'#type' => 'textfield',
'#title' => t('Label'),
'#maxlength' => 255,
'#default_value' => $comment_type->label(),
'#required' => TRUE,
);
$form['id'] = array(
'#type' => 'machine_name',
'#default_value' => $comment_type->id(),
'#machine_name' => array(
'exists' => '\Drupal\comment\Entity\CommentType::load',
),
'#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
'#disabled' => !$comment_type->isNew(),
);
$form['description'] = array(
'#type' => 'textarea',
'#default_value' => $comment_type->getDescription(),
'#description' => t('Describe this comment type. The text will be displayed on the <em>Comment types</em> administration overview page'),
'#title' => t('Description'),
);
if ($comment_type->isNew()) {
$options = array();
foreach ($this->entityManager->getDefinitions() as $entity_type) {
// Only expose entities that have field UI enabled, only those can
// get comment fields added in the UI.
if ($entity_type->get('field_ui_base_route')) {
$options[$entity_type->id()] = $entity_type->getLabel();
}
}
$form['target_entity_type_id'] = array(
'#type' => 'select',
'#default_value' => $comment_type->getTargetEntityTypeId(),
'#title' => t('Target entity type'),
'#options' => $options,
'#description' => t('The target entity type can not be changed after the comment type has been created.')
);
}
else {
$form['target_entity_type_id_display'] = array(
'#type' => 'item',
'#markup' => $this->entityManager->getDefinition($comment_type->getTargetEntityTypeId())->getLabel(),
'#title' => t('Target entity type'),
);
}
if ($this->moduleHandler->moduleExists('content_translation')) {
$form['language'] = array(
'#type' => 'details',
'#title' => t('Language settings'),
'#group' => 'additional_settings',
);
$language_configuration = ContentLanguageSettings::loadByEntityTypeBundle('comment', $comment_type->id());
$form['language']['language_configuration'] = array(
'#type' => 'language_configuration',
'#entity_information' => array(
'entity_type' => 'comment',
'bundle' => $comment_type->id(),
),
'#default_value' => $language_configuration,
);
$form['#submit'][] = 'language_configuration_element_submit';
}
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Save'),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$comment_type = $this->entity;
$status = $comment_type->save();
$edit_link = $this->entity->link($this->t('Edit'));
if ($status == SAVED_UPDATED) {
drupal_set_message(t('Comment type %label has been updated.', array('%label' => $comment_type->label())));
$this->logger->notice('Comment type %label has been updated.', array('%label' => $comment_type->label(), 'link' => $edit_link));
}
else {
$this->commentManager->addBodyField($comment_type->id());
drupal_set_message(t('Comment type %label has been added.', array('%label' => $comment_type->label())));
$this->logger->notice('Comment type %label has been added.', array('%label' => $comment_type->label(), 'link' => $edit_link));
}
$form_state->setRedirectUrl($comment_type->urlInfo('collection'));
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentTypeInterface.
*/
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,53 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentTypeListBuilder.
*/
namespace Drupal\comment;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines a class to build a listing of comment type entities.
*
* @see \Drupal\comment\Entity\CommentType
*/
class CommentTypeListBuilder extends ConfigEntityListBuilder {
/**
* {@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');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['type'] = SafeMarkup::checkPlain($entity->label());
$row['description'] = Xss::filterAdmin($entity->getDescription());
return $row + parent::buildRow($entity);
}
}

View file

@ -0,0 +1,192 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentViewBuilder.
*/
namespace Drupal\comment;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityViewBuilder;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Render controller for comments.
*/
class CommentViewBuilder extends EntityViewBuilder {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new CommentViewBuilder.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager, AccountInterface $current_user) {
parent::__construct($entity_type, $entity_manager, $language_manager);
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity.manager'),
$container->get('language_manager'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langcode) {
$build = parent::getBuildDefaults($entity, $view_mode, $langcode);
/** @var \Drupal\comment\CommentInterface $entity */
// Store a threading field setting to use later in self::buildComponents().
$build['#comment_threaded'] = $entity->getCommentedEntity()
->getFieldDefinition($entity->getFieldName())
->getSetting('default_mode') === CommentManagerInterface::COMMENT_MODE_THREADED;
// If threading is enabled, don't render cache individual comments, but do
// keep the cache tags, so they can bubble up.
if ($build['#comment_threaded']) {
$cache_tags = $build['#cache']['tags'];
$build['#cache'] = [];
$build['#cache']['tags'] = $cache_tags;
}
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, $langcode = NULL) {
/** @var \Drupal\comment\CommentInterface[] $entities */
if (empty($entities)) {
return;
}
// Pre-load associated users into cache to leverage multiple loading.
$uids = array();
foreach ($entities as $entity) {
$uids[] = $entity->getOwnerId();
}
$this->entityManager->getStorage('user')->loadMultiple(array_unique($uids));
parent::buildComponents($build, $entities, $displays, $view_mode, $langcode);
// A counter to track the indentation level.
$current_indent = 0;
foreach ($entities as $id => $entity) {
if ($build[$id]['#comment_threaded']) {
$comment_indent = count(explode('.', $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();
$build[$id]['#entity'] = $entity;
$build[$id]['#theme'] = 'comment__' . $entity->getFieldName() . '__' . $commented_entity->bundle();
$display = $displays[$entity->bundle()];
if ($display->getComponent('links')) {
$build[$id]['links'] = array(
'#lazy_builder' => ['comment.lazy_builders:renderLinks', [
$entity->id(),
$view_mode,
$langcode,
!empty($entity->in_preview),
]],
'#create_placeholder' => TRUE,
);
}
if (!isset($build[$id]['#attached'])) {
$build[$id]['#attached'] = array();
}
$build[$id]['#attached']['library'][] = 'comment/drupal.comment-by-viewer';
if ($this->moduleHandler->moduleExists('history') && $this->currentUser->isAuthenticated()) {
$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' => ['history_attach_timestamp', [$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, $langcode = NULL) {
parent::alterBuild($build, $comment, $display, $view_mode, $langcode);
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">';
}
// Add anchor for each comment.
$prefix .= "<a id=\"comment-{$comment->id()}\"></a>\n";
$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,335 @@
<?php
/**
* @file
* Contains \Drupal\comment\CommentViewsData.
*/
namespace Drupal\comment;
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'] = 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'] = t('Title');
$data['comment_field_data']['subject']['help'] = t('The title of the comment.');
$data['comment_field_data']['name']['title'] = t('Author');
$data['comment_field_data']['name']['help'] = 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'] = t("Author's website");
$data['comment_field_data']['homepage']['help'] = 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'] = t('Email of user that posted the comment. Will be empty if the author is a registered user.');
$data['comment_field_data']['created']['title'] = t('Post date');
$data['comment_field_data']['created']['help'] = t('Date and time of when the comment was created.');
$data['comment_field_data']['changed']['title'] = t('Updated date');
$data['comment_field_data']['changed']['help'] = t('Date and time of when the comment was last updated.');
$data['comment_field_data']['changed_fulldata'] = array(
'title' => t('Created date'),
'help' => t('Date in the form of CCYYMMDD.'),
'argument' => array(
'field' => 'changed',
'id' => 'date_fulldate',
),
);
$data['comment_field_data']['changed_year_month'] = array(
'title' => t('Created year + month'),
'help' => t('Date in the form of YYYYMM.'),
'argument' => array(
'field' => 'changed',
'id' => 'date_year_month',
),
);
$data['comment_field_data']['changed_year'] = array(
'title' => t('Created year'),
'help' => t('Date in the form of YYYY.'),
'argument' => array(
'field' => 'changed',
'id' => 'date_year',
),
);
$data['comment_field_data']['changed_month'] = array(
'title' => t('Created month'),
'help' => t('Date in the form of MM (01 - 12).'),
'argument' => array(
'field' => 'changed',
'id' => 'date_month',
),
);
$data['comment_field_data']['changed_day'] = array(
'title' => t('Created day'),
'help' => t('Date in the form of DD (01 - 31).'),
'argument' => array(
'field' => 'changed',
'id' => 'date_day',
),
);
$data['comment_field_data']['changed_week'] = array(
'title' => t('Created week'),
'help' => t('Date in the form of WW (01 - 53).'),
'argument' => array(
'field' => 'changed',
'id' => 'date_week',
),
);
$data['comment_field_data']['status']['title'] = t('Approved status');
$data['comment_field_data']['status']['help'] = t('Whether the comment is approved (or still in the moderation queue).');
$data['comment_field_data']['status']['filter']['label'] = t('Approved comment status');
$data['comment_field_data']['status']['filter']['type'] = 'yes-no';
$data['comment']['approve_comment'] = array(
'field' => array(
'title' => t('Link to approve comment'),
'help' => t('Provide a simple link to approve the comment.'),
'id' => 'comment_link_approve',
),
);
$data['comment']['replyto_comment'] = array(
'field' => array(
'title' => t('Link to reply-to comment'),
'help' => t('Provide a simple link to reply to the comment.'),
'id' => 'comment_link_reply',
),
);
$data['comment_field_data']['thread']['field'] = array(
'title' => t('Depth'),
'help' => t('Display the depth of the comment if it is threaded.'),
'id' => 'comment_depth',
);
$data['comment_field_data']['thread']['sort'] = array(
'title' => t('Thread'),
'help' => 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::entityManager()->getDefinitions();
// Provide a relationship for each entity type except comment.
foreach ($entities_types as $type => $entity_type) {
if ($type == 'comment' || !$entity_type->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface') || !$entity_type->getBaseTable()) {
continue;
}
if ($fields = \Drupal::service('comment.manager')->getFields($type)) {
$data['comment_field_data'][$type] = array(
'relationship' => array(
'title' => $entity_type->getLabel(),
'help' => t('The @entity_type to which the comment is a reply to.', array('@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' => array(
array(
'field' => 'entity_type',
'value' => $type,
'table' => 'comment_field_data'
),
),
),
);
}
}
$data['comment_field_data']['uid']['title'] = t('Author uid');
$data['comment_field_data']['uid']['help'] = t('If you need more fields than the uid add the comment: author relationship');
$data['comment_field_data']['uid']['relationship']['title'] = t('Author');
$data['comment_field_data']['uid']['relationship']['help'] = t("The User ID of the comment's author.");
$data['comment_field_data']['uid']['relationship']['label'] = t('author');
$data['comment_field_data']['pid']['title'] = t('Parent CID');
$data['comment_field_data']['pid']['relationship']['title'] = t('Parent comment');
$data['comment_field_data']['pid']['relationship']['help'] = t('The parent comment');
$data['comment_field_data']['pid']['relationship']['label'] = 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'] = t('Comment Statistics');
// Provide a relationship for each entity type except comment.
foreach ($entities_types as $type => $entity_type) {
if ($type == 'comment' || !$entity_type->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface') || !$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()] = array(
'type' => 'INNER',
'left_field' => $entity_type->getKey('id'),
'field' => 'entity_id',
'extra' => array(
array(
'field' => 'entity_type',
'value' => $type,
),
),
);
}
}
$data['comment_entity_statistics']['last_comment_timestamp'] = array(
'title' => t('Last comment time'),
'help' => t('Date and time of when the last comment was posted.'),
'field' => array(
'id' => 'comment_last_timestamp',
),
'sort' => array(
'id' => 'date',
),
'filter' => array(
'id' => 'date',
),
);
$data['comment_entity_statistics']['last_comment_name'] = array(
'title' => t("Last comment author"),
'help' => t('The name of the author of the last posted comment.'),
'field' => array(
'id' => 'comment_ces_last_comment_name',
'no group by' => TRUE,
),
'sort' => array(
'id' => 'comment_ces_last_comment_name',
'no group by' => TRUE,
),
);
$data['comment_entity_statistics']['comment_count'] = array(
'title' => t('Comment count'),
'help' => t('The number of comments an entity has.'),
'field' => array(
'id' => 'numeric',
),
'filter' => array(
'id' => 'numeric',
),
'sort' => array(
'id' => 'standard',
),
'argument' => array(
'id' => 'standard',
),
);
$data['comment_entity_statistics']['last_updated'] = array(
'title' => t('Updated/commented date'),
'help' => t('The most recent of last comment posted or entity updated time.'),
'field' => array(
'id' => 'comment_ces_last_updated',
'no group by' => TRUE,
),
'sort' => array(
'id' => 'comment_ces_last_updated',
'no group by' => TRUE,
),
'filter' => array(
'id' => 'comment_ces_last_updated',
),
);
$data['comment_entity_statistics']['cid'] = array(
'title' => t('Last comment CID'),
'help' => t('Display the last comment of an entity'),
'relationship' => array(
'title' => t('Last comment'),
'help' => t('The last comment of an entity.'),
'group' => t('Comment'),
'base' => 'comment',
'base field' => 'cid',
'id' => 'standard',
'label' => t('Last Comment'),
),
);
$data['comment_entity_statistics']['last_comment_uid'] = array(
'title' => t('Last comment uid'),
'help' => t('The User ID of the author of the last comment of an entity.'),
'relationship' => array(
'title' => t('Last comment author'),
'base' => 'users',
'base field' => 'uid',
'id' => 'standard',
'label' => t('Last comment author'),
),
'filter' => array(
'id' => 'numeric',
),
'argument' => array(
'id' => 'numeric',
),
'field' => array(
'id' => 'numeric',
),
);
$data['comment_entity_statistics']['entity_type'] = array(
'title' => t('Entity type'),
'help' => t('The entity type to which the comment is a reply to.'),
'field' => array(
'id' => 'standard',
),
'filter' => array(
'id' => 'string',
),
'argument' => array(
'id' => 'string',
),
'sort' => array(
'id' => 'standard',
),
);
$data['comment_entity_statistics']['field_name'] = array(
'title' => t('Comment field name'),
'help' => t('The field name from which the comment originated.'),
'field' => array(
'id' => 'standard',
),
'filter' => array(
'id' => 'string',
),
'argument' => array(
'id' => 'string',
),
'sort' => array(
'id' => 'standard',
),
);
return $data;
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* @file
* Contains \Drupal\comment\Controller\AdminController.
*/
namespace Drupal\comment\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for comment module administrative routes.
*/
class AdminController extends ControllerBase {
/**
* The form builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('form_builder')
);
}
/**
* Constructs an AdminController object.
*
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
*/
public function __construct(FormBuilderInterface $form_builder) {
$this->formBuilder = $form_builder;
}
/**
* Presents an administrative comment listing.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request of the page.
* @param string $type
* The type of the overview form ('approval' or 'new') default to 'new'.
*
* @return array
* Then comment multiple delete confirmation form or the comments overview
* administration form.
*/
public function adminPage(Request $request, $type = 'new') {
if ($request->request->get('operation') == 'delete' && $request->request->get('comments')) {
return $this->formBuilder->getForm('\Drupal\comment\Form\ConfirmDeleteMultiple', $request);
}
else {
return $this->formBuilder->getForm('\Drupal\comment\Form\CommentAdminOverview', $type);
}
}
}

View file

@ -0,0 +1,338 @@
<?php
/**
* @file
* Contains \Drupal\comment\Controller\CommentController.
*/
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\Controller\ControllerBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
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 manager.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $entityManager;
/**
* 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\EntityManagerInterface $entity_manager
* The entity manager service.
*/
public function __construct(HttpKernelInterface $http_kernel, CommentManagerInterface $comment_manager, EntityManagerInterface $entity_manager) {
$this->httpKernel = $http_kernel;
$this->commentManager = $comment_manager;
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('http_kernel'),
$container->get('comment.manager'),
$container->get('entity.manager')
);
}
/**
* Publishes the specified comment.
*
* @param \Drupal\comment\CommentInterface $comment
* A comment entity.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse.
* Redirects to the permalink URL for this comment.
*/
public function commentApprove(CommentInterface $comment) {
$comment->setPublished(TRUE);
$comment->save();
drupal_set_message($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.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*
* @return \Symfony\Component\HttpFoundation\Response
* The comment listing set to the page on which the comment appears.
*/
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->entityManager()->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle())[$comment->getFieldName()];
// Find the current display page for this comment.
$page = $this->entityManager()->getStorage('comment')->getDisplayOrdinal($comment, $field_definition->getSetting('default_mode'), $field_definition->getSetting('per_page'));
// @todo: Cleaner sub request handling.
$redirect_request = Request::create($entity->url(), 'GET', $request->query->all(), $request->cookies->all(), array(), $request->server->all());
$redirect_request->query->set('page', $page);
// Carry over the session to the subrequest.
if ($session = $request->getSession()) {
$redirect_request->setSession($session);
}
// @todo: Convert the pager to use the request object.
$request->query->set('page', $page);
return $this->httpKernel->handle($redirect_request, HttpKernelInterface::SUB_REQUEST);
}
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->entityManager()->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.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Redirects user to new url.
*/
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', array(
'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.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* @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.
*/
public function getReplyForm(Request $request, EntityInterface $entity, $field_name, $pid = NULL) {
$account = $this->currentUser();
$uri = $entity->urlInfo()->setAbsolute();
$build = array();
// 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->entityManager()->getStorage('comment')->load($pid);
// Display the parent comment.
$build['comment_parent'] = $this->entityManager()->getViewBuilder('comment')->view($comment);
}
// The comment is in response to a 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->entityManager()->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->entityManager()->getStorage('comment')->create(array(
'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.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* @return \Drupal\Core\Access\AccessResultInterface
* An access result
*/
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');
$status = $entity->{$field_name}->status;
$access = $access->andIf(AccessResult::allowedIf($status == CommentItemInterface::OPEN)
->cacheUntilEntityChanges($entity));
// $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->entityManager()->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->cacheUntilEntityChanges($comment);
}
}
return $access;
}
/**
* Returns a set of nodes' last read timestamps.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request of the page.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The JSON response.
*/
public function renderNewCommentsNodeLinks(Request $request) {
if ($this->currentUser()->isAnonymous()) {
throw new AccessDeniedHttpException();
}
$nids = $request->request->get('node_ids');
$field_name = $request->request->get('field_name');
if (!isset($nids)) {
throw new NotFoundHttpException();
}
// Only handle up to 100 nodes.
$nids = array_slice($nids, 0, 100);
$links = array();
foreach ($nids as $nid) {
$node = $this->entityManager->getStorage('node')->load($nid);
$new = $this->commentManager->getCountNewComments($node);
$page_number = $this->entityManager()->getStorage('comment')
->getNewCommentPageNumber($node->{$field_name}->comment_count, $new, $node);
$query = $page_number ? array('page' => $page_number) : NULL;
$links[$nid] = array(
'new_comment_count' => (int) $new,
'first_new_comment_link' => $this->getUrlGenerator()->generateFromPath('node/' . $node->id(), array('query' => $query, 'fragment' => 'new')),
);
}
return new JsonResponse($links);
}
}

View file

@ -0,0 +1,587 @@
<?php
/**
* @file
* Contains \Drupal\comment\Entity\Comment.
*/
namespace Drupal\comment\Entity;
use Drupal\Component\Utility\Number;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\comment\CommentInterface;
use Drupal\Core\Entity\EntityChangedTrait;
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\UserInterface;
/**
* Defines the comment entity class.
*
* @ContentEntityType(
* id = "comment",
* label = @Translation("Comment"),
* bundle_label = @Translation("Content type"),
* handlers = {
* "storage" = "Drupal\comment\CommentStorage",
* "storage_schema" = "Drupal\comment\CommentStorageSchema",
* "access" = "Drupal\comment\CommentAccessControlHandler",
* "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"
* },
* links = {
* "canonical" = "/comment/{comment}",
* "delete-form" = "/comment/{comment}/delete",
* "edit-form" = "/comment/{comment}/edit",
* },
* bundle_entity_type = "comment_type",
* field_ui_base_route = "entity.comment_type.edit_form",
* constraints = {
* "CommentName" = {}
* }
* )
*/
class Comment extends ContentEntityBase implements CommentInterface {
use EntityChangedTrait;
/**
* The thread for which a lock was acquired.
*/
protected $threadLock = '';
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
if (is_null($this->get('status')->value)) {
$published = \Drupal::currentUser()->hasPermission('skip comment approval') ? CommentInterface::PUBLISHED : CommentInterface::NOT_PUBLISHED;
$this->setPublished($published);
}
if ($this->isNew()) {
// Add the comment to database. This next section builds the thread field.
// Also see the documentation for comment_view().
$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($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;
}
// We test the value with '===' because we need to modify anonymous
// users as well.
if ($this->getOwnerId() === \Drupal::currentUser()->id() && \Drupal::currentUser()->isAuthenticated()) {
$this->setAuthorName(\Drupal::currentUser()->getUsername());
}
// Add the values which aren't passed into the function.
$this->setThread($thread);
$this->setHostname(\Drupal::request()->getClientIP());
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
$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);
entity_delete_multiple('comment', $child_cids);
foreach ($entities as $id => $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() {
$entity = $this->getCommentedEntity();
$uri = $entity->urlInfo();
$uri->setOption('fragment', 'comment-' . $this->id());
return $uri;
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['cid'] = BaseFieldDefinition::create('integer')
->setLabel(t('Comment ID'))
->setDescription(t('The comment ID.'))
->setReadOnly(TRUE)
->setSetting('unsigned', TRUE);
$fields['uuid'] = BaseFieldDefinition::create('uuid')
->setLabel(t('UUID'))
->setDescription(t('The comment UUID.'))
->setReadOnly(TRUE);
$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['langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Language'))
->setDescription(t('The comment language code.'))
->setTranslatable(TRUE)
->setDisplayOptions('view', array(
'type' => 'hidden',
))
->setDisplayOptions('form', array(
'type' => 'language_select',
'weight' => 2,
));
$fields['subject'] = BaseFieldDefinition::create('string')
->setLabel(t('Subject'))
->setTranslatable(TRUE)
->setSetting('max_length', 64)
->setDisplayOptions('form', array(
'type' => 'string_textfield',
// Default comment body field has weight 20.
'weight' => 10,
))
->setDisplayConfigurable('form', TRUE);
$fields['uid'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('User ID'))
->setDescription(t('The user ID of the comment author.'))
->setTranslatable(TRUE)
->setSetting('target_type', 'user')
->setDefaultValue(0);
$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);
$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['status'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Publishing status'))
->setDescription(t('A boolean indicating whether the comment is published.'))
->setTranslatable(TRUE)
->setDefaultValue(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'))
->setDescription(t('The entity type to which this comment is attached.'))
->setSetting('is_ascii', TRUE)
->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH);
$fields['comment_type'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Comment Type'))
->setDescription(t('The comment type.'))
->setSetting('target_type', 'comment_type');
$fields['field_name'] = BaseFieldDefinition::create('string')
->setLabel(t('Comment field name'))
->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 array();
}
/**
* {@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 ($this->get('uid')->target_id) {
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 isPublished() {
return $this->get('status')->value == CommentInterface::PUBLISHED;
}
/**
* {@inheritdoc}
*/
public function getStatus() {
return $this->get('status')->value;
}
/**
* {@inheritdoc}
*/
public function setPublished($status) {
$this->set('status', $status ? CommentInterface::PUBLISHED : CommentInterface::NOT_PUBLISHED);
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 function getChangedTime() {
return $this->get('changed')->value;
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageInterface $storage, array &$values) {
if (empty($values['comment_type']) && !empty($values['field_name']) && !empty($values['entity_type'])) {
$field_storage = FieldStorageConfig::loadByName($values['entity_type'], $values['field_name']);
$values['comment_type'] = $field_storage->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;
}
/**
* {@inheritdoc}
*/
public function getOwnerId() {
return $this->get('uid')->target_id;
}
/**
* {@inheritdoc}
*/
public function setOwnerId($uid) {
$this->set('uid', $uid);
return $this;
}
/**
* {@inheritdoc}
*/
public function setOwner(UserInterface $account) {
$this->set('uid', $account->id());
return $this;
}
/**
* Get the comment type ID for this comment.
*
* @return string
* The ID of the comment type.
*/
public function getTypeId() {
return $this->bundle();
}
}

View file

@ -0,0 +1,102 @@
<?php
/**
* @file
* Contains \Drupal\comment\Entity\CommentType.
*/
namespace Drupal\comment\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\comment\CommentTypeInterface;
/**
* Defines the comment type entity.
*
* @ConfigEntityType(
* id = "comment_type",
* label = @Translation("Comment type"),
* handlers = {
* "form" = {
* "default" = "Drupal\comment\CommentTypeForm",
* "add" = "Drupal\comment\CommentTypeForm",
* "edit" = "Drupal\comment\CommentTypeForm",
* "delete" = "Drupal\comment\Form\CommentTypeDeleteForm"
* },
* "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",
* "collection" = "/admin/structure/comment/types",
* },
* 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,282 @@
<?php
/**
* @file
* Contains \Drupal\comment\Form\CommentAdminOverview.
*/
namespace Drupal\comment\Form;
use Drupal\comment\CommentInterface;
use Drupal\comment\CommentStorageInterface;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the comments overview administration form.
*/
class CommentAdminOverview extends FormBase {
/**
* The entity storage.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The comment storage.
*
* @var \Drupal\comment\CommentStorageInterface
*/
protected $commentStorage;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Creates a CommentAdminOverview form.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\comment\CommentStorageInterface $comment_storage
* The comment storage.
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date formatter service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(EntityManagerInterface $entity_manager, CommentStorageInterface $comment_storage, DateFormatter $date_formatter, ModuleHandlerInterface $module_handler) {
$this->entityManager = $entity_manager;
$this->commentStorage = $comment_storage;
$this->dateFormatter = $date_formatter;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager'),
$container->get('entity.manager')->getStorage('comment'),
$container->get('date.formatter'),
$container->get('module_handler')
);
}
/**
* {@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'] = array(
'#type' => 'details',
'#title' => $this->t('Update options'),
'#open' => TRUE,
'#attributes' => array('class' => array('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'] = array(
'#type' => 'select',
'#title' => $this->t('Action'),
'#title_display' => 'invisible',
'#options' => $options,
'#default_value' => 'publish',
);
$form['options']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Update'),
);
// Load the comments that need to be displayed.
$status = ($type == 'approval') ? CommentInterface::NOT_PUBLISHED : CommentInterface::PUBLISHED;
$header = array(
'subject' => array(
'data' => $this->t('Subject'),
'specifier' => 'subject',
),
'author' => array(
'data' => $this->t('Author'),
'specifier' => 'name',
'class' => array(RESPONSIVE_PRIORITY_MEDIUM),
),
'posted_in' => array(
'data' => $this->t('Posted in'),
'class' => array(RESPONSIVE_PRIORITY_LOW),
),
'changed' => array(
'data' => $this->t('Updated'),
'specifier' => 'changed',
'sort' => 'desc',
'class' => array(RESPONSIVE_PRIORITY_LOW),
),
'operations' => $this->t('Operations'),
);
$cids = $this->commentStorage->getQuery()
->condition('status', $status)
->tableSort($header)
->pager(50)
->execute();
/** @var $comments \Drupal\comment\CommentInterface[] */
$comments = $this->commentStorage->loadMultiple($cids);
// Build a table listing the appropriate comments.
$options = array();
$destination = $this->getDestinationArray();
$commented_entity_ids = array();
$commented_entities = array();
foreach ($comments as $comment) {
$commented_entity_ids[$comment->getCommentedEntityTypeId()][] = $comment->getCommentedEntityId();
}
foreach ($commented_entity_ids as $entity_type => $ids) {
$commented_entities[$entity_type] = $this->entityManager->getStorage($entity_type)->loadMultiple($ids);
}
foreach ($comments as $comment) {
/** @var $commented_entity \Drupal\Core\Entity\EntityInterface */
$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') ?: array();
$attributes += array('title' => Unicode::truncate($body, 128));
$comment_permalink->setOption('attributes', $attributes);
}
$options[$comment->id()] = array(
'title' => array('data' => array('#title' => $comment->getSubject() ?: $comment->id())),
'subject' => array(
'data' => array(
'#type' => 'link',
'#title' => $comment->getSubject(),
'#url' => $comment_permalink,
),
),
'author' => array(
'data' => array(
'#theme' => 'username',
'#account' => $comment->getOwner(),
),
),
'posted_in' => array(
'data' => array(
'#type' => 'link',
'#title' => $commented_entity->label(),
'#access' => $commented_entity->access('view'),
'#url' => $commented_entity->urlInfo(),
),
),
'changed' => $this->dateFormatter->format($comment->getChangedTimeAcrossTranslations(), 'short'),
);
$comment_uri_options = $comment->urlInfo()->getOptions() + ['query' => $destination];
$links = array();
$links['edit'] = array(
'title' => $this->t('Edit'),
'url' => $comment->urlInfo('edit-form', $comment_uri_options),
);
if ($this->moduleHandler->moduleExists('content_translation') && $this->moduleHandler->invoke('content_translation', 'translate_access', array($comment))->isAllowed()) {
$links['translate'] = array(
'title' => $this->t('Translate'),
'url' => $comment->urlInfo('drupal:content-translation-overview', $comment_uri_options),
);
}
$options[$comment->id()]['operations']['data'] = array(
'#type' => 'operations',
'#links' => $links,
);
}
$form['comments'] = array(
'#type' => 'tableselect',
'#header' => $header,
'#options' => $options,
'#empty' => $this->t('No comments available.'),
);
$form['pager'] = array('#type' => 'pager');
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$form_state->setValue('comments', array_diff($form_state->getValue('comments'), array(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');
foreach ($cids as $cid) {
// Delete operation handled in \Drupal\comment\Form\ConfirmDeleteMultiple
// see \Drupal\comment\Controller\AdminController::adminPage().
if ($operation == 'unpublish') {
$comment = $this->commentStorage->load($cid);
$comment->setPublished(FALSE);
$comment->save();
}
elseif ($operation == 'publish') {
$comment = $this->commentStorage->load($cid);
$comment->setPublished(TRUE);
$comment->save();
}
}
drupal_set_message($this->t('The update has been performed.'));
$form_state->setRedirect('comment.admin');
}
}

View file

@ -0,0 +1,120 @@
<?php
/**
* @file
* Contains \Drupal\comment\Form\CommentTypeDeleteForm.
*/
namespace Drupal\comment\Form;
use Drupal\comment\CommentManagerInterface;
use Drupal\Core\Entity\EntityDeleteForm;
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldStorageConfig;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a confirmation form for deleting a comment type entity.
*/
class CommentTypeDeleteForm extends EntityDeleteForm {
/**
* The query factory to create entity queries.
*
* @var \Drupal\Core\Entity\Query\QueryFactory
*/
public $queryFactory;
/**
* The comment manager service.
*
* @var \Drupal\comment\CommentManagerInterface
*/
protected $commentManager;
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $entityManager;
/**
* 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\Core\Entity\Query\QueryFactory $query_factory
* The entity query object.
* @param \Drupal\comment\CommentManagerInterface $comment_manager
* The comment manager service.
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager service.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
*/
public function __construct(QueryFactory $query_factory, CommentManagerInterface $comment_manager, EntityManager $entity_manager, LoggerInterface $logger) {
$this->queryFactory = $query_factory;
$this->commentManager = $comment_manager;
$this->entityManager = $entity_manager;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.query'),
$container->get('comment.manager'),
$container->get('entity.manager'),
$container->get('logger.factory')->get('comment')
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$comments = $this->queryFactory->get('comment')->condition('comment_type', $this->entity->id())->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.', array(
'%label' => $this->entity->label(),
'%field' => $field_storage->label(),
)) . '</p>';
}
}
if (!empty($comments)) {
$caption .= '<p>' . $this->formatPlural(count($comments), '%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.', array('%label' => $this->entity->label())) . '</p>';
}
if ($caption) {
$form['description'] = array('#markup' => $caption);
return $form;
}
else {
return parent::buildForm($form, $form_state);
}
}
}

View file

@ -0,0 +1,130 @@
<?php
/**
* @file
* Contains \Drupal\comment\Form\ConfirmDeleteMultiple.
*/
namespace Drupal\comment\Form;
use Drupal\comment\CommentStorageInterface;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the comment multiple delete confirmation form.
*/
class ConfirmDeleteMultiple extends ConfirmFormBase {
/**
* The comment storage.
*
* @var \Drupal\comment\CommentStorageInterface
*/
protected $commentStorage;
/**
* An array of comments to be deleted.
*
* @var \Drupal\comment\CommentInterface[]
*/
protected $comments;
/**
* Creates an new ConfirmDeleteMultiple form.
*
* @param \Drupal\comment\CommentStorageInterface $comment_storage
* The comment storage.
*/
public function __construct(CommentStorageInterface $comment_storage) {
$this->commentStorage = $comment_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager')->getStorage('comment')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'comment_multiple_delete_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete these comments and all their children?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('comment.admin');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete comments');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$edit = $form_state->getUserInput();
$form['comments'] = array(
'#prefix' => '<ul>',
'#suffix' => '</ul>',
'#tree' => TRUE,
);
// array_filter() returns only elements with actual values.
$comment_counter = 0;
$this->comments = $this->commentStorage->loadMultiple(array_keys(array_filter($edit['comments'])));
foreach ($this->comments as $comment) {
$cid = $comment->id();
$form['comments'][$cid] = array(
'#type' => 'hidden',
'#value' => $cid,
'#prefix' => '<li>',
'#suffix' => SafeMarkup::checkPlain($comment->label()) . '</li>'
);
$comment_counter++;
}
$form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
if (!$comment_counter) {
drupal_set_message($this->t('There do not appear to be any comments to delete, or your selected comment was deleted by another administrator.'));
$form_state->setRedirect('comment.admin');
}
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getValue('confirm')) {
$this->commentStorage->delete($this->comments);
$count = count($form_state->getValue('comments'));
$this->logger('content')->notice('Deleted @count comments.', array('@count' => $count));
drupal_set_message($this->formatPlural($count, 'Deleted 1 comment.', 'Deleted @count comments.'));
}
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* @file
* Contains \Drupal\comment\Form\DeleteForm.
*/
namespace Drupal\comment\Form;
use Drupal\Core\Entity\ContentEntityDeleteForm;
/**
* Provides the comment delete confirmation form.
*/
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->urlInfo();
}
/**
* {@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('content')->notice('Deleted comment @cid and its replies.', array('@cid' => $this->entity->id()));
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\Action\PublishComment.
*/
namespace Drupal\comment\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
/**
* Publishes a comment.
*
* @Action(
* id = "comment_publish_action",
* label = @Translation("Publish comment"),
* type = "comment"
* )
*/
class PublishComment extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute($comment = NULL) {
$comment->setPublished(TRUE);
$comment->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\comment\CommentInterface $object */
$result = $object->status->access('edit', $account, TRUE)
->andIf($object->access('update', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\Action\SaveComment.
*/
namespace Drupal\comment\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
/**
* Saves a comment.
*
* @Action(
* id = "comment_save_action",
* label = @Translation("Save comment"),
* type = "comment"
* )
*/
class SaveComment extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute($comment = NULL) {
$comment->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\comment\CommentInterface $object */
return $object->access('update', $account, $return_as_object);
}
}

View file

@ -0,0 +1,81 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\Action\UnpublishByKeywordComment.
*/
namespace Drupal\comment\Plugin\Action;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Unpublishes a comment containing certain keywords.
*
* @Action(
* id = "comment_unpublish_by_keyword_action",
* label = @Translation("Unpublish comment containing keyword(s)"),
* type = "comment"
* )
*/
class UnpublishByKeywordComment extends ConfigurableActionBase {
/**
* {@inheritdoc}
*/
public function execute($comment = NULL) {
$build = comment_view($comment);
$text = \Drupal::service('renderer')->renderPlain($build);
foreach ($this->configuration['keywords'] as $keyword) {
if (strpos($text, $keyword) !== FALSE) {
$comment->setPublished(FALSE);
$comment->save();
break;
}
}
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return array(
'keywords' => array(),
);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['keywords'] = array(
'#title' => t('Keywords'),
'#type' => 'textarea',
'#description' => t('The comment will be unpublished if it contains any of the phrases above. Use a case-sensitive, comma-separated list of phrases. Example: funny, bungee jumping, "Company, Inc."'),
'#default_value' => Tags::implode($this->configuration['keywords']),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['keywords'] = Tags::explode($form_state->getValue('keywords'));
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\comment\CommentInterface $object */
$result = $object->access('update', $account, TRUE)
->andIf($object->status->access('edit', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\Action\UnpublishComment.
*/
namespace Drupal\comment\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
/**
* Unpublishes a comment.
*
* @Action(
* id = "comment_unpublish_action",
* label = @Translation("Unpublish comment"),
* type = "comment"
* )
*/
class UnpublishComment extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute($comment = NULL) {
$comment->setPublished(FALSE);
$comment->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\comment\CommentInterface $object */
$result = $object->status->access('edit', $account, TRUE)
->andIf($object->access('update', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}

View file

@ -0,0 +1,69 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\EntityReferenceSelection\CommentSelection.
*/
namespace Drupal\comment\Plugin\EntityReferenceSelection;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\SelectionBase;
use Drupal\comment\CommentInterface;
/**
* Provides specific access control for the comment entity type.
*
* @EntityReferenceSelection(
* id = "default:comment",
* label = @Translation("Comment selection"),
* entity_types = {"comment"},
* group = "default",
* weight = 1
* )
*/
class CommentSelection extends SelectionBase {
/**
* {@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 entityQueryAlter(SelectInterface $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");
}
// The Comment module doesn't implement any proper comment access,
// and as a consequence doesn't make sure that comments cannot be viewed
// when the user doesn't have access to the node.
$node_alias = $query->innerJoin('node_field_data', 'n', '%alias.nid = ' . $data_table . '.entity_id AND ' . $data_table . ".entity_type = 'node'");
// Pass the query to the node access control.
$this->reAlterQuery($query, 'node_access', $node_alias);
// Passing the query to node_query_node_access_alter() is sadly
// insufficient for nodes.
// @see SelectionEntityTypeNode::entityQueryAlter()
if (!$this->currentUser->hasPermission('bypass node access') && !count($this->moduleHandler->getImplementations('node_grants'))) {
$query->condition($node_alias . '.status', 1);
}
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\Field\FieldFormatter\AuthorNameFormatter.
*/
namespace Drupal\comment\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
/**
* Plugin implementation of the 'comment_username' formatter.
*
* @FieldFormatter(
* id = "comment_username",
* label = @Translation("Author name"),
* description = @Translation("Display the author name."),
* field_types = {
* "string"
* }
* )
*/
class AuthorNameFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items) {
$elements = array();
foreach ($items as $delta => $item) {
/** @var $comment \Drupal\comment\CommentInterface */
$comment = $item->getEntity();
$account = $comment->getOwner();
$elements[$delta] = array(
'#theme' => 'username',
'#account' => $account,
'#cache' => array(
'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,254 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter.
*/
namespace Drupal\comment\Plugin\Field\FieldFormatter;
use Drupal\comment\CommentManagerInterface;
use Drupal\comment\CommentStorageInterface;
use Drupal\comment\Entity\Comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityViewBuilderInterface;
use Drupal\Core\Entity\EntityFormBuilderInterface;
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\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a default comment formatter.
*
* @FieldFormatter(
* id = "comment_default",
* module = "comment",
* label = @Translation("Comment list"),
* field_types = {
* "comment"
* },
* quickedit = {
* "editor" = "disabled"
* }
* )
*/
class CommentDefaultFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'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 manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The entity form builder.
*
* @var \Drupal\Core\Entity\EntityFormBuilderInterface
*/
protected $entityFormBuilder;
/**
* {@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.manager'),
$container->get('entity.form_builder')
);
}
/**
* 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\EntityManagerInterface $entity_manager
* The entity manager
* @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder
* The entity form builder.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, EntityManagerInterface $entity_manager, EntityFormBuilderInterface $entity_form_builder) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->viewBuilder = $entity_manager->getViewBuilder('comment');
$this->storage = $entity_manager->getStorage('comment');
$this->currentUser = $current_user;
$this->entityManager = $entity_manager;
$this->entityFormBuilder = $entity_form_builder;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items) {
$elements = array();
$output = array();
$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, array('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')) {
// This is a listing of Comment entities, so associate its list cache
// tag for correct invalidation.
$output['comments']['#cache']['tags'] = $this->entityManager->getDefinition('comment')->getListCacheTags();
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);
$build['pager']['#type'] = 'pager';
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')) {
// All users in the "anonymous" role can use the same form: it is fine
// for this form to be stored in the render cache.
if ($this->currentUser->isAnonymous()) {
$comment = $this->storage->create(array(
'entity_type' => $entity->getEntityTypeId(),
'entity_id' => $entity->id(),
'field_name' => $field_name,
'comment_type' => $this->getFieldSetting('comment_type'),
'pid' => NULL,
));
$output['comment_form'] = $this->entityFormBuilder->getForm($comment);
}
// All other users need a user-specific form, which would break the
// render cache: hence use a #lazy_builder callback.
else {
$output['comment_form'] = [
'#lazy_builder' => ['comment.lazy_builders:renderForm', [
$entity->getEntityTypeId(),
$entity->id(),
$field_name,
$this->getFieldSetting('comment_type'),
]],
'#create_placeholder' => TRUE,
];
}
}
}
$elements[] = $output + array(
'#comment_type' => $this->getFieldSetting('comment_type'),
'#comment_display_mode' => $this->getFieldSetting('default_mode'),
'comments' => array(),
'comment_form' => array(),
);
}
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element = array();
$element['pager_id'] = array(
'#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() {
// Only show a summary if we're using a non-standard pager id.
if ($this->getSetting('pager_id')) {
return array($this->t('Pager ID: @id', array(
'@id' => $this->getSetting('pager_id'),
)));
}
return array();
}
}

View file

@ -0,0 +1,211 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\Field\FieldType\CommentItem.
*/
namespace Drupal\comment\Plugin\Field\FieldType;
use Drupal\comment\CommentManagerInterface;
use Drupal\comment\Entity\CommentType;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\UrlGeneratorTrait;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Session\AnonymousUserSession;
/**
* Plugin implementation of the 'comment' field type.
*
* @FieldType(
* id = "comment",
* label = @Translation("Comments"),
* description = @Translation("This field manages configuration and presentation of comments on an entity."),
* list_class = "\Drupal\comment\CommentFieldItemList",
* default_widget = "comment_default",
* default_formatter = "comment_default"
* )
*/
class CommentItem extends FieldItemBase implements CommentItemInterface {
use UrlGeneratorTrait;
/**
* {@inheritdoc}
*/
public static function defaultStorageSettings() {
return array(
'comment_type' => '',
) + parent::defaultStorageSettings();
}
/**
* {@inheritdoc}
*/
public static function defaultFieldSettings() {
return array(
'default_mode' => CommentManagerInterface::COMMENT_MODE_THREADED,
'per_page' => 50,
'form_location' => CommentItemInterface::FORM_BELOW,
'anonymous' => COMMENT_ANONYMOUS_MAYNOT_CONTACT,
'preview' => DRUPAL_OPTIONAL,
) + parent::defaultFieldSettings();
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['status'] = DataDefinition::create('integer')
->setLabel(t('Comment status'))
->setRequired(TRUE);
$properties['cid'] = DataDefinition::create('integer')
->setLabel(t('Last comment ID'));
$properties['last_comment_timestamp'] = DataDefinition::create('integer')
->setLabel(t('Last comment timestamp'))
->setDescription(t('The time that the last comment was created.'));
$properties['last_comment_name'] = DataDefinition::create('string')
->setLabel(t('Last comment name'))
->setDescription(t('The name of the user posting the last comment.'));
$properties['last_comment_uid'] = DataDefinition::create('integer')
->setLabel(t('Last comment user ID'));
$properties['comment_count'] = DataDefinition::create('integer')
->setLabel(t('Number of comments'))
->setDescription(t('The number of comments.'));
return $properties;
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return array(
'columns' => array(
'status' => array(
'description' => 'Whether comments are allowed on this entity: 0 = no, 1 = closed (read only), 2 = open (read/write).',
'type' => 'int',
'default' => 0,
),
),
'indexes' => array(),
'foreign keys' => array(),
);
}
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
$element = array();
$settings = $this->getSettings();
$anonymous_user = new AnonymousUserSession();
$element['default_mode'] = array(
'#type' => 'checkbox',
'#title' => t('Threading'),
'#default_value' => $settings['default_mode'],
'#description' => t('Show comment replies in a threaded list.'),
);
$element['per_page'] = array(
'#type' => 'number',
'#title' => t('Comments per page'),
'#default_value' => $settings['per_page'],
'#required' => TRUE,
'#min' => 10,
'#max' => 1000,
'#step' => 10,
);
$element['anonymous'] = array(
'#type' => 'select',
'#title' => t('Anonymous commenting'),
'#default_value' => $settings['anonymous'],
'#options' => array(
COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'),
COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'),
COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information'),
),
'#access' => $anonymous_user->hasPermission('post comments'),
);
$element['form_location'] = array(
'#type' => 'checkbox',
'#title' => t('Show reply form on the same page as comments'),
'#default_value' => $settings['form_location'],
);
$element['preview'] = array(
'#type' => 'radios',
'#title' => t('Preview comment'),
'#default_value' => $settings['preview'],
'#options' => array(
DRUPAL_DISABLED => t('Disabled'),
DRUPAL_OPTIONAL => t('Optional'),
DRUPAL_REQUIRED => t('Required'),
),
);
return $element;
}
/**
* {@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 = array();
// @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 = array();
$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'] = array(
'#type' => 'select',
'#title' => 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>.', array('@url' => $this->url('entity.comment_type.collection'))),
'#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,40 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\Field\FieldType\CommentItemInterface.
*/
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,109 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\Field\FieldWidget\CommentWidget.
*/
namespace Drupal\comment\Plugin\Field\FieldWidget;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a default comment widget.
*
* @FieldWidget(
* id = "comment_default",
* label = @Translation("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'] = array(
'#type' => 'radios',
'#title' => t('Comments'),
'#title_display' => 'invisible',
'#default_value' => $items->status,
'#options' => array(
CommentItemInterface::OPEN => t('Open'),
CommentItemInterface::CLOSED => t('Closed'),
CommentItemInterface::HIDDEN => t('Hidden'),
),
CommentItemInterface::OPEN => array(
'#description' => t('Users with the "Post comments" permission can post comments.'),
),
CommentItemInterface::CLOSED => array(
'#description' => t('Users cannot post comments, but existing comments will be displayed.'),
),
CommentItemInterface::HIDDEN => array(
'#description' => t('Comments are hidden from view.'),
),
);
// If the entity doesn't have any comments, the "hidden" 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) {
$element['status'][CommentItemInterface::HIDDEN]['#access'] = FALSE;
// Also adjust the description of the "closed" option.
$element['status'][CommentItemInterface::CLOSED]['#description'] = t('Users cannot post comments.');
}
// 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 += array(
'#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' => array(
'class' => array('comment-' . Html::getClass($entity->getEntityTypeId()) . '-settings-form'),
),
'#attached' => array(
'library' => array('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 += array(
'cid' => 0,
'last_comment_timestamp' => 0,
'last_comment_name' => '',
'last_comment_uid' => 0,
'comment_count' => 0,
);
}
return $values;
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\Menu\LocalTask\UnapprovedComments.
*/
namespace Drupal\comment\Plugin\Menu\LocalTask;
use Drupal\comment\CommentStorageInterface;
use Drupal\Core\Menu\LocalTaskDefault;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a local task that shows the amount of unapproved comments.
*/
class UnapprovedComments extends LocalTaskDefault implements ContainerFactoryPluginInterface {
/**
* 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.manager')->getStorage('comment')
);
}
/**
* {@inheritdoc}
*/
public function getTitle() {
return t('Unapproved comments (@count)', array('@count' => $this->commentStorage->getUnapprovedCount()));
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\Validation\Constraint\CommentNameConstraint.
*/
namespace Drupal\comment\Plugin\Validation\Constraint;
use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
/**
* Supports validating comment author names.
*
* @Constraint(
* id = "CommentName",
* label = @Translation("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,106 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\Validation\Constraint\CommentNameConstraintValidator.
*/
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 {
/**
* Validator 2.5 and upwards compatible execution context.
*
* @var \Symfony\Component\Validator\Context\ExecutionContextInterface
*/
protected $context;
/**
* 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.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(array('name' => $author_name));
if (!empty($users)) {
$this->context->buildViolation($constraint->messageNameTaken, array('%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->getUsername() != $author_name) {
$this->context->buildViolation($constraint->messageMatch)
->atPath('name')
->addViolation();
}
}
// Anonymous account might be required - depending on field settings.
if ($owner_id === 0 && empty($author_name) &&
$this->getAnonymousContactDetailsSetting($entity) === COMMENT_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,114 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\argument\UserUid.
*/
namespace Drupal\comment\Plugin\views\argument;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Database\Connection;
use Drupal\views\Plugin\views\argument\ArgumentPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Argument handler to accept a user id to check for nodes that
* user posted or commented on.
*
* @ingroup views_argument_handlers
*
* @ViewsArgument("argument_comment_user_uid")
*/
class UserUid extends ArgumentPluginBase {
/**
* Database Service Object.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Constructs a Drupal\Component\Plugin\PluginBase 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'));
}
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', array(':uid' => $this->argument))->fetchField();
}
if (empty($title)) {
return $this->t('No user');
}
return SafeMarkup::checkPlain($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 = db_or()
->condition("$this->tableAlias.uid", $this->argument, '=')
->exists($subselect);
$this->query->addWhere(0, $condition);
}
}
/**
* {@inheritdoc}
*/
public function getSortName() {
return $this->t('Numerical', array(), array('context' => 'Sort order'));
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\field\Depth.
*/
namespace Drupal\comment\Plugin\views\field;
use Drupal\views\Plugin\views\field\Field;
use Drupal\views\ResultRow;
/**
* Field handler to display the depth of a comment.
*
* @ingroup views_field_handlers
*
* @ViewsField("comment_depth")
*/
class Depth extends Field {
/**
* {@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']['#markup'];
$item['rendered']['#markup'] = count(explode('.', $comment_thread)) - 1;
}
return $items;
}
}

View file

@ -0,0 +1,83 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\field\EntityLink.
*/
namespace Drupal\comment\Plugin\views\field;
use Drupal\Core\Form\FormStateInterface;
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 node_view_multiple for all rows to reuse it later.
*
* @var array
*/
protected $build;
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['teaser'] = array('default' => FALSE);
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['teaser'] = array(
'#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 = array();
foreach ($values as $row) {
$entity = $row->_entity;
$entities[$entity->id()] = $entity;
}
if ($entities) {
$this->build = entity_view_multiple($entities, $this->options['teaser'] ? 'teaser' : 'full');
}
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
$entity = $this->getEntity($values);
// Only render the links, if they are defined.
return !empty($this->build[$entity->id()]['links']['comment__comment']) ? drupal_render($this->build[$entity->id()]['links']['comment__comment']) : '';
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\field\LastTimestamp.
*/
namespace Drupal\comment\Plugin\views\field;
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 {
/**
* Overrides Drupal\views\Plugin\views\field\FieldPluginBase::init().
*/
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,45 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\field\LinkApprove.
*/
namespace Drupal\comment\Plugin\views\field;
use Drupal\Core\Url;
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) {
return Url::fromRoute('comment.approve', ['comment' => $this->getEntity($row)->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,44 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\field\LinkReply.
*/
namespace Drupal\comment\Plugin\views\field;
use Drupal\Core\Url;
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);
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,192 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\field\NodeNewComments.
*/
namespace Drupal\comment\Plugin\views\field;
use Drupal\Core\Database\Connection;
use Drupal\comment\CommentInterface;
use Drupal\Core\Form\FormStateInterface;
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;
/**
* Constructs a Drupal\Component\Plugin\PluginBase 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'));
}
/**
* {@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'] = array('table' => 'comment_entity_statistics', 'field' => 'comment_count');
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['link_to_comment'] = array('default' => TRUE);
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['link_to_comment'] = array(
'#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 = array();
$ids = array();
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']}] = array();
}
$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", array(
':status' => CommentInterface::PUBLISHED,
':h_uid' => $user->id(),
':nids[]' => $nids,
':timestamp1' => HISTORY_READ_LIMIT,
':timestamp2' => HISTORY_READ_LIMIT,
));
foreach ($result as $node) {
foreach ($ids[$node->id()] 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 = entity_create('node', array(
'nid' => $this->getValue($values, 'nid'),
'type' => $this->getValue($values, 'type'),
));
$page_number = \Drupal::entityManager()->getStorage('comment')
->getNewCommentPageNumber($this->getValue($values, 'comment_count'), $this->getValue($values), $node);
$this->options['alter']['make_link'] = TRUE;
$this->options['alter']['url'] = $node->urlInfo();
$this->options['alter']['query'] = $page_number ? array('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,84 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\field\StatisticsLastCommentName.
*/
namespace Drupal\comment\Plugin\views\field;
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 {
/**
* {@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 = array(
'table' => 'users_field_data',
'field' => 'uid',
'left_table' => 'comment_entity_statistics',
'left_field' => 'last_comment_uid',
'extra' => array(
array(
'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'] = array('default' => TRUE);
return $options;
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
if (!empty($this->options['link_to_user'])) {
$account = entity_create('user');
$account->name = $this->getValue($values);
$account->uid = $values->{$this->uid};
$username = array(
'#theme' => 'username',
'#account' => $account,
);
return drupal_render($username);
}
else {
return $this->sanitizeValue($this->getValue($values));
}
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\field\StatisticsLastUpdated.
*/
namespace Drupal\comment\Plugin\views\field;
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 {
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,30 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\filter\NodeComment.
*/
namespace Drupal\comment\Plugin\views\filter;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\views\Plugin\views\filter\InOperator;
/**
* Filter based on comment node status.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("node_comment")
*/
class NodeComment extends InOperator {
public function getValueOptions() {
$this->valueOptions = array(
CommentItemInterface::HIDDEN => $this->t('Hidden'),
CommentItemInterface::CLOSED => $this->t('Closed'),
CommentItemInterface::OPEN => $this->t('Open'),
);
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\filter\StatisticsLastUpdated.
*/
namespace Drupal\comment\Plugin\views\filter;
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 {
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,41 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\filter\UserUid.
*/
namespace Drupal\comment\Plugin\views\filter;
use Drupal\views\Plugin\views\filter\FilterPluginBase;
/**
* Filter handler to accept a user id to check for nodes that user posted or
* commented on.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("comment_user_uid")
*/
class UserUid extends FilterPluginBase {
public function query() {
$this->ensureMyTable();
$subselect = db_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 = db_or()
->condition("$this->tableAlias.uid", $this->value, $this->operator)
->exists($subselect);
$this->query->addWhere($this->options['group'], $condition);
}
}

View file

@ -0,0 +1,139 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\row\Rss.
*/
namespace Drupal\comment\Plugin\views\row;
use Drupal\views\Plugin\views\row\RssPluginBase;
/**
* Plugin which formats the comments as RSS items.
*
* @ViewsRow(
* id = "comment_rss",
* title = @Translation("Comment"),
* help = @Translation("Display the comment as RSS."),
* theme = "views_view_row_rss",
* register_theme = FALSE,
* base = {"comment_field_data"},
* display_types = {"feed"}
* )
*/
class Rss extends RssPluginBase {
/**
* {@inheritdoc}
*/
protected $base_table = 'comment_field_data';
/**
* {@inheritdoc}
*/
protected $base_field = 'cid';
/**
* @var \Drupal\comment\CommentInterface[]
*/
protected $comments;
/**
* {@inheritdoc}
*/
protected $entityTypeId = 'comment';
public function preRender($result) {
$cids = array();
foreach ($result as $row) {
$cids[] = $row->cid;
}
$this->comments = $this->entityManager->getStorage('comment')->loadMultiple($cids);
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm_summary_options() {
$options = parent::buildOptionsForm_summary_options();
$options['title'] = $this->t('Title only');
$options['default'] = $this->t('Use site default RSS settings');
return $options;
}
public function render($row) {
global $base_url;
$cid = $row->{$this->field_alias};
if (!is_numeric($cid)) {
return;
}
$view_mode = $this->options['view_mode'];
if ($view_mode == 'default') {
$view_mode = \Drupal::config('system.rss')->get('items.view_mode');
}
// Load the specified comment and its associated node:
/** @var $comment \Drupal\comment\CommentInterface */
$comment = $this->comments[$cid];
if (empty($comment)) {
return;
}
$description_build = [];
$comment->link = $comment->url('canonical', array('absolute' => TRUE));
$comment->rss_namespaces = array();
$comment->rss_elements = array(
array(
'key' => 'pubDate',
'value' => gmdate('r', $comment->getCreatedTime()),
),
array(
'key' => 'dc:creator',
'value' => $comment->getAuthorName(),
),
array(
'key' => 'guid',
'value' => 'comment ' . $comment->id() . ' at ' . $base_url,
'attributes' => array('isPermaLink' => 'false'),
),
);
// The comment gets built and modules add to or modify
// $comment->rss_elements and $comment->rss_namespaces.
$build = comment_view($comment, 'rss');
unset($build['#theme']);
if (!empty($comment->rss_namespaces)) {
$this->view->style_plugin->namespaces = array_merge($this->view->style_plugin->namespaces, $comment->rss_namespaces);
}
if ($view_mode != 'title') {
// We render comment contents.
$description_build = $build;
}
$item = new \stdClass();
$item->description = $description_build;
$item->title = $comment->label();
$item->link = $comment->link;
// Provide a reference so that the render call in
// template_preprocess_views_view_row_rss() can still access it.
$item->elements = &$comment->rss_elements;
$item->cid = $comment->id();
$build = array(
'#theme' => $this->themeFunctions(),
'#view' => $this->view,
'#options' => $this->options,
'#row' => $item,
);
return $build;
}
}

View file

@ -0,0 +1,42 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\sort\StatisticsLastCommentName.
*/
namespace Drupal\comment\Plugin\views\sort;
use Drupal\views\Plugin\views\sort\SortPluginBase;
/**
* Sort handler to sort by last comment name which might be in 2 different
* fields.
*
* @ingroup views_sort_handlers
*
* @ViewsSort("comment_ces_last_comment_name")
*/
class StatisticsLastCommentName extends SortPluginBase {
public function query() {
$this->ensureMyTable();
$definition = array(
'table' => 'users_field_data',
'field' => 'uid',
'left_table' => 'comment_entity_statistics',
'left_field' => 'last_comment_uid',
);
$join = \Drupal::service('plugin.manager.views.join')->createInstance('standard', $definition);
// @todo this might be safer if we had an ensure_relationship rather than guessing
// the table alias. Though if we did that we'd be guessing the relationship name
// so that doesn't matter that much.
$this->user_table = $this->query->ensureTable('ces_users', $this->relationship, $join);
$this->user_field = $this->query->addField($this->user_table, 'name');
// Add the field.
$this->query->addOrderBy(NULL, "LOWER(COALESCE($this->user_table.name, $this->tableAlias.$this->field))", $this->options['order'], $this->tableAlias . '_' . $this->field);
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\sort\StatisticsLastUpdated.
*/
namespace Drupal\comment\Plugin\views\sort;
use Drupal\views\Plugin\views\sort\Date;
/**
* Sort handler for the newer of last comment / entity updated.
*
* @ingroup views_sort_handlers
*
* @ViewsSort("comment_ces_last_updated")
*/
class StatisticsLastUpdated extends Date {
public function query() {
$this->ensureMyTable();
$this->node_table = $this->query->ensureTable('node', $this->relationship);
$this->field_alias = $this->query->addOrderBy(NULL, "GREATEST(" . $this->node_table . ".changed, " . $this->tableAlias . ".last_comment_timestamp)", $this->options['order'], $this->tableAlias . '_' . $this->field);
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\sort\Thread.
*/
namespace Drupal\comment\Plugin\views\sort;
use Drupal\views\Plugin\views\sort\SortPluginBase;
/**
* Sort handler for ordering by thread.
*
* @ingroup views_sort_handlers
*
* @ViewsSort("comment_thread")
*/
class Thread extends SortPluginBase {
public function query() {
$this->ensureMyTable();
//Read comment_render() in comment.module for an explanation of the
//thinking behind this sort.
if ($this->options['order'] == 'DESC') {
$this->query->addOrderBy($this->tableAlias, $this->realField, $this->options['order']);
}
else {
$alias = $this->tableAlias . '_' . $this->realField . 'asc';
//@todo is this secure?
$this->query->addOrderBy(NULL, "SUBSTRING({$this->tableAlias}.{$this->realField}, 1, (LENGTH({$this->tableAlias}.{$this->realField}) - 1))", $this->options['order'], $alias);
}
}
}

View file

@ -0,0 +1,111 @@
<?php
/**
* @file
* Contains \Drupal\comment\Plugin\views\wizard\Comment.
*/
namespace Drupal\comment\Plugin\views\wizard;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\wizard\WizardPluginBase;
/**
* @todo: replace numbers with constants.
*/
/**
* Tests creating comment views with the wizard.
*
* @ViewsWizard(
* id = "comment",
* base_table = "comment_field_data",
* title = @Translation("Comments")
* )
*/
class Comment extends WizardPluginBase {
/**
* Set the created column.
*/
protected $createdColumn = 'created';
/**
* Set default values for the filters.
*/
protected $filters = array(
'status' => array(
'value' => TRUE,
'table' => 'comment_field_data',
'field' => 'status',
'plugin_id' => 'boolean',
'entity_type' => 'comment',
'entity_field' => 'status',
),
'status_node' => array(
'value' => TRUE,
'table' => 'node_field_data',
'field' => 'status',
'plugin_id' => 'boolean',
'relationship' => 'node',
'entity_type' => 'node',
'entity_field' => 'status',
),
);
/**
* Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::rowStyleOptions().
*/
protected function rowStyleOptions() {
$options = array();
$options['entity:comment'] = $this->t('comments');
$options['fields'] = $this->t('fields');
return $options;
}
/**
* Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::defaultDisplayOptions().
*/
protected function defaultDisplayOptions() {
$display_options = parent::defaultDisplayOptions();
// Add permission-based access control.
$display_options['access']['type'] = 'perm';
$display_options['access']['options']['perm'] = 'access comments';
// Add a relationship to nodes.
$display_options['relationships']['node']['id'] = 'node';
$display_options['relationships']['node']['table'] = 'comment_field_data';
$display_options['relationships']['node']['field'] = 'node';
$display_options['relationships']['node']['entity_type'] = 'comment_field_data';
$display_options['relationships']['node']['required'] = 1;
$display_options['relationships']['node']['plugin_id'] = 'standard';
// Remove the default fields, since we are customizing them here.
unset($display_options['fields']);
/* Field: Comment: Title */
$display_options['fields']['subject']['id'] = 'subject';
$display_options['fields']['subject']['table'] = 'comment_field_data';
$display_options['fields']['subject']['field'] = 'subject';
$display_options['fields']['subject']['entity_type'] = 'comment';
$display_options['fields']['subject']['entity_field'] = 'subject';
$display_options['fields']['subject']['label'] = '';
$display_options['fields']['subject']['alter']['alter_text'] = 0;
$display_options['fields']['subject']['alter']['make_link'] = 0;
$display_options['fields']['subject']['alter']['absolute'] = 0;
$display_options['fields']['subject']['alter']['trim'] = 0;
$display_options['fields']['subject']['alter']['word_boundary'] = 0;
$display_options['fields']['subject']['alter']['ellipsis'] = 0;
$display_options['fields']['subject']['alter']['strip_tags'] = 0;
$display_options['fields']['subject']['alter']['html'] = 0;
$display_options['fields']['subject']['hide_empty'] = 0;
$display_options['fields']['subject']['empty_zero'] = 0;
$display_options['fields']['subject']['plugin_id'] = 'field';
$display_options['fields']['subject']['type'] = 'string';
$display_options['fields']['subject']['settings'] = ['link_to_entity' => TRUE];
return $display_options;
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentActionsTest.
*/
namespace Drupal\comment\Tests;
use Drupal\comment\Entity\Comment;
/**
* Tests actions provided by the Comment module.
*
* @group comment
*/
class CommentActionsTest extends CommentTestBase {
/**
* Modules to install.
*
* @var array
*/
public static $modules = array('dblog', 'action');
/**
* Tests comment publish and unpublish actions.
*/
function testCommentPublishUnpublishActions() {
$this->drupalLogin($this->webUser);
$comment_text = $this->randomMachineName();
$subject = $this->randomMachineName();
$comment = $this->postComment($this->node, $comment_text, $subject);
// Unpublish a comment.
$action = entity_load('action', 'comment_unpublish_action');
$action->execute(array($comment));
$this->assertTrue($comment->isPublished() === FALSE, 'Comment was unpublished');
// Publish a comment.
$action = entity_load('action', 'comment_publish_action');
$action->execute(array($comment));
$this->assertTrue($comment->isPublished() === TRUE, 'Comment was published');
}
/**
* Tests the unpublish comment by keyword action.
*/
function testCommentUnpublishByKeyword() {
$this->drupalLogin($this->adminUser);
$keyword_1 = $this->randomMachineName();
$keyword_2 = $this->randomMachineName();
$action = entity_create('action', array(
'id' => 'comment_unpublish_by_keyword_action',
'label' => $this->randomMachineName(),
'type' => 'comment',
'configuration' => array(
'keywords' => array($keyword_1, $keyword_2),
),
'plugin' => 'comment_unpublish_by_keyword_action',
));
$action->save();
$comment = $this->postComment($this->node, $keyword_2, $this->randomMachineName());
// Load the full comment so that status is available.
$comment = Comment::load($comment->id());
$this->assertTrue($comment->isPublished() === TRUE, 'The comment status was set to published.');
$action->execute(array($comment));
$this->assertTrue($comment->isPublished() === FALSE, 'The comment status was set to not published.');
}
}

View file

@ -0,0 +1,214 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentAdminTest.
*/
namespace Drupal\comment\Tests;
use Drupal\user\RoleInterface;
/**
* Tests comment approval functionality.
*
* @group comment
*/
class CommentAdminTest extends CommentTestBase {
/**
* Test comment approval functionality through admin/content/comment.
*/
function testApprovalAdminInterface() {
// Set anonymous comments to require approval.
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, array(
'access comments' => TRUE,
'post comments' => TRUE,
'skip comment approval' => FALSE,
));
$this->drupalLogin($this->adminUser);
$this->setCommentAnonymous('0'); // Ensure that doesn't require contact info.
// Test that the comments page loads correctly when there are no comments
$this->drupalGet('admin/content/comment');
$this->assertText(t('No comments available.'));
$this->drupalLogout();
// Post anonymous comment without contact info.
$subject = $this->randomMachineName();
$body = $this->randomMachineName();
$this->postComment($this->node, $body, $subject, TRUE); // Set $contact to true so that it won't check for id and message.
$this->assertText(t('Your comment has been queued for review by site administrators and will be published after approval.'), 'Comment requires approval.');
// Get unapproved comment id.
$this->drupalLogin($this->adminUser);
$anonymous_comment4 = $this->getUnapprovedComment($subject);
$anonymous_comment4 = entity_create('comment', array(
'cid' => $anonymous_comment4,
'subject' => $subject,
'comment_body' => $body,
'entity_id' => $this->node->id(),
'entity_type' => 'node',
'field_name' => 'comment'
));
$this->drupalLogout();
$this->assertFalse($this->commentExists($anonymous_comment4), 'Anonymous comment was not published.');
// Approve comment.
$this->drupalLogin($this->adminUser);
$this->performCommentOperation($anonymous_comment4, 'publish', TRUE);
$this->drupalLogout();
$this->drupalGet('node/' . $this->node->id());
$this->assertTrue($this->commentExists($anonymous_comment4), 'Anonymous comment visible.');
// Post 2 anonymous comments without contact info.
$comments[] = $this->postComment($this->node, $this->randomMachineName(), $this->randomMachineName(), TRUE);
$comments[] = $this->postComment($this->node, $this->randomMachineName(), $this->randomMachineName(), TRUE);
// Publish multiple comments in one operation.
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/content/comment/approval');
$this->assertText(t('Unapproved comments (@count)', array('@count' => 2)), 'Two unapproved comments waiting for approval.');
$edit = array(
"comments[{$comments[0]->id()}]" => 1,
"comments[{$comments[1]->id()}]" => 1,
);
$this->drupalPostForm(NULL, $edit, t('Update'));
$this->assertText(t('Unapproved comments (@count)', array('@count' => 0)), 'All comments were approved.');
// Delete multiple comments in one operation.
$edit = array(
'operation' => 'delete',
"comments[{$comments[0]->id()}]" => 1,
"comments[{$comments[1]->id()}]" => 1,
"comments[{$anonymous_comment4->id()}]" => 1,
);
$this->drupalPostForm(NULL, $edit, t('Update'));
$this->assertText(t('Are you sure you want to delete these comments and all their children?'), 'Confirmation required.');
$this->drupalPostForm(NULL, $edit, t('Delete comments'));
$this->assertText(t('No comments available.'), 'All comments were deleted.');
// Test message when no comments selected.
$edit = array(
'operation' => 'delete',
);
$this->drupalPostForm(NULL, $edit, t('Update'));
$this->assertText(t('Select one or more comments to perform the update on.'));
}
/**
* Tests comment approval functionality through the node interface.
*/
function testApprovalNodeInterface() {
// Set anonymous comments to require approval.
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, array(
'access comments' => TRUE,
'post comments' => TRUE,
'skip comment approval' => FALSE,
));
$this->drupalLogin($this->adminUser);
$this->setCommentAnonymous('0'); // Ensure that doesn't require contact info.
$this->drupalLogout();
// Post anonymous comment without contact info.
$subject = $this->randomMachineName();
$body = $this->randomMachineName();
$this->postComment($this->node, $body, $subject, TRUE); // Set $contact to true so that it won't check for id and message.
$this->assertText(t('Your comment has been queued for review by site administrators and will be published after approval.'), 'Comment requires approval.');
// Get unapproved comment id.
$this->drupalLogin($this->adminUser);
$anonymous_comment4 = $this->getUnapprovedComment($subject);
$anonymous_comment4 = entity_create('comment', array(
'cid' => $anonymous_comment4,
'subject' => $subject,
'comment_body' => $body,
'entity_id' => $this->node->id(),
'entity_type' => 'node',
'field_name' => 'comment'
));
$this->drupalLogout();
$this->assertFalse($this->commentExists($anonymous_comment4), 'Anonymous comment was not published.');
// Approve comment.
$this->drupalLogin($this->adminUser);
$this->drupalGet('comment/1/approve');
$this->assertResponse(403, 'Forged comment approval was denied.');
$this->drupalGet('comment/1/approve', array('query' => array('token' => 'forged')));
$this->assertResponse(403, 'Forged comment approval was denied.');
$this->drupalGet('comment/1/edit');
$this->assertFieldChecked('edit-status-0');
$this->drupalGet('node/' . $this->node->id());
$this->clickLink(t('Approve'));
$this->drupalLogout();
$this->drupalGet('node/' . $this->node->id());
$this->assertTrue($this->commentExists($anonymous_comment4), 'Anonymous comment visible.');
}
/**
* Tests comment bundle admin.
*/
public function testCommentAdmin() {
// Login.
$this->drupalLogin($this->adminUser);
// Browse to comment bundle overview.
$this->drupalGet('admin/structure/comment');
$this->assertResponse(200);
// Make sure titles visible.
$this->assertText('Comment type');
$this->assertText('Description');
// Make sure the description is present.
$this->assertText('Default comment field');
// Manage fields.
$this->clickLink('Manage fields');
$this->assertResponse(200);
// Make sure comment_body field is shown.
$this->assertText('comment_body');
// Rest from here on in is field_ui.
}
/**
* Tests editing a comment as an admin.
*/
public function testEditComment() {
// Enable anonymous user comments.
user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, array(
'access comments',
'post comments',
'skip comment approval',
));
// Login as a web user.
$this->drupalLogin($this->webUser);
// Post a comment.
$comment = $this->postComment($this->node, $this->randomMachineName());
$this->drupalLogout();
// Post anonymous comment.
$this->drupalLogin($this->adminUser);
$this->setCommentAnonymous('2'); // Ensure that we need email id before posting comment.
$this->drupalLogout();
// Post comment with contact info (required).
$author_name = $this->randomMachineName();
$author_mail = $this->randomMachineName() . '@example.com';
$anonymous_comment = $this->postComment($this->node, $this->randomMachineName(), $this->randomMachineName(), array('name' => $author_name, 'mail' => $author_mail));
// Login as an admin user.
$this->drupalLogin($this->adminUser);
// Make sure the comment field is not visible when
// the comment was posted by an authenticated user.
$this->drupalGet('comment/' . $comment->id() . '/edit');
$this->assertNoFieldById('edit-mail', $comment->getAuthorEmail());
// Make sure the comment field is visible when
// the comment was posted by an anonymous user.
$this->drupalGet('comment/' . $anonymous_comment->id() . '/edit');
$this->assertFieldById('edit-mail', $anonymous_comment->getAuthorEmail());
}
}

View file

@ -0,0 +1,170 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentAnonymousTest.
*/
namespace Drupal\comment\Tests;
use Drupal\user\RoleInterface;
/**
* Tests anonymous commenting.
*
* @group comment
*/
class CommentAnonymousTest extends CommentTestBase {
protected function setUp() {
parent::setUp();
// Enable anonymous and authenticated user comments.
user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, array(
'access comments',
'post comments',
'skip comment approval',
));
user_role_grant_permissions(RoleInterface::AUTHENTICATED_ID, array(
'access comments',
'post comments',
'skip comment approval',
));
}
/**
* Tests anonymous comment functionality.
*/
function testAnonymous() {
$this->drupalLogin($this->adminUser);
$this->setCommentAnonymous(COMMENT_ANONYMOUS_MAYNOT_CONTACT);
$this->drupalLogout();
// Post anonymous comment without contact info.
$anonymous_comment1 = $this->postComment($this->node, $this->randomMachineName(), $this->randomMachineName());
$this->assertTrue($this->commentExists($anonymous_comment1), 'Anonymous comment without contact info found.');
// Allow contact info.
$this->drupalLogin($this->adminUser);
$this->setCommentAnonymous(COMMENT_ANONYMOUS_MAY_CONTACT);
// Attempt to edit anonymous comment.
$this->drupalGet('comment/' . $anonymous_comment1->id() . '/edit');
$edited_comment = $this->postComment(NULL, $this->randomMachineName(), $this->randomMachineName());
$this->assertTrue($this->commentExists($edited_comment, FALSE), 'Modified reply found.');
$this->drupalLogout();
// Post anonymous comment with contact info (optional).
$this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment');
$this->assertTrue($this->commentContactInfoAvailable(), 'Contact information available.');
// Check the presence of expected cache tags.
$this->assertCacheTag('config:field.field.node.article.comment');
$this->assertCacheTag('config:user.settings');
$anonymous_comment2 = $this->postComment($this->node, $this->randomMachineName(), $this->randomMachineName());
$this->assertTrue($this->commentExists($anonymous_comment2), 'Anonymous comment with contact info (optional) found.');
// Ensure anonymous users cannot post in the name of registered users.
$edit = array(
'name' => $this->adminUser->getUsername(),
'mail' => $this->randomMachineName() . '@example.com',
'subject[0][value]' => $this->randomMachineName(),
'comment_body[0][value]' => $this->randomMachineName(),
);
$this->drupalPostForm('comment/reply/node/' . $this->node->id() . '/comment', $edit, t('Save'));
$this->assertRaw(t('The name you used (%name) belongs to a registered user.', [
'%name' => $this->adminUser->getUsername(),
]));
// Require contact info.
$this->drupalLogin($this->adminUser);
$this->setCommentAnonymous(COMMENT_ANONYMOUS_MUST_CONTACT);
$this->drupalLogout();
// Try to post comment with contact info (required).
$this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment');
$this->assertTrue($this->commentContactInfoAvailable(), 'Contact information available.');
$anonymous_comment3 = $this->postComment($this->node, $this->randomMachineName(), $this->randomMachineName(), TRUE);
// Name should have 'Anonymous' for value by default.
$this->assertText(t('Email field is required.'), 'Email required.');
$this->assertFalse($this->commentExists($anonymous_comment3), 'Anonymous comment with contact info (required) not found.');
// Post comment with contact info (required).
$author_name = $this->randomMachineName();
$author_mail = $this->randomMachineName() . '@example.com';
$anonymous_comment3 = $this->postComment($this->node, $this->randomMachineName(), $this->randomMachineName(), array('name' => $author_name, 'mail' => $author_mail));
$this->assertTrue($this->commentExists($anonymous_comment3), 'Anonymous comment with contact info (required) found.');
// Make sure the user data appears correctly when editing the comment.
$this->drupalLogin($this->adminUser);
$this->drupalGet('comment/' . $anonymous_comment3->id() . '/edit');
$this->assertRaw($author_name, "The anonymous user's name is correct when editing the comment.");
$this->assertRaw($author_mail, "The anonymous user's email address is correct when editing the comment.");
// Unpublish comment.
$this->performCommentOperation($anonymous_comment3, 'unpublish');
$this->drupalGet('admin/content/comment/approval');
$this->assertRaw('comments[' . $anonymous_comment3->id() . ']', 'Comment was unpublished.');
// Publish comment.
$this->performCommentOperation($anonymous_comment3, 'publish', TRUE);
$this->drupalGet('admin/content/comment');
$this->assertRaw('comments[' . $anonymous_comment3->id() . ']', 'Comment was published.');
// Delete comment.
$this->performCommentOperation($anonymous_comment3, 'delete');
$this->drupalGet('admin/content/comment');
$this->assertNoRaw('comments[' . $anonymous_comment3->id() . ']', 'Comment was deleted.');
$this->drupalLogout();
// Comment 3 was deleted.
$this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment/' . $anonymous_comment3->id());
$this->assertResponse(403);
// Reset.
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, array(
'access comments' => FALSE,
'post comments' => FALSE,
'skip comment approval' => FALSE,
));
// Attempt to view comments while disallowed.
// NOTE: if authenticated user has permission to post comments, then a
// "Login or register to post comments" type link may be shown.
$this->drupalGet('node/' . $this->node->id());
$this->assertNoPattern('@<h2[^>]*>Comments</h2>@', 'Comments were not displayed.');
$this->assertNoLink('Add new comment', 'Link to add comment was found.');
// Attempt to view node-comment form while disallowed.
$this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment');
$this->assertResponse(403);
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, array(
'access comments' => TRUE,
'post comments' => FALSE,
'skip comment approval' => FALSE,
));
$this->drupalGet('node/' . $this->node->id());
$this->assertPattern('@<h2[^>]*>Comments</h2>@', 'Comments were displayed.');
$this->assertLink('Log in', 1, 'Link to log in was found.');
$this->assertLink('register', 1, 'Link to register was found.');
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, array(
'access comments' => FALSE,
'post comments' => TRUE,
'skip comment approval' => TRUE,
));
$this->drupalGet('node/' . $this->node->id());
$this->assertNoPattern('@<h2[^>]*>Comments</h2>@', 'Comments were not displayed.');
$this->assertFieldByName('subject[0][value]', '', 'Subject field found.');
$this->assertFieldByName('comment_body[0][value]', '', 'Comment field found.');
$this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment/' . $anonymous_comment2->id());
$this->assertResponse(403);
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentBlockTest.
*/
namespace Drupal\comment\Tests;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\user\RoleInterface;
/**
* Tests comment block functionality.
*
* @group comment
*/
class CommentBlockTest extends CommentTestBase {
/**
* Modules to install.
*
* @var array
*/
public static $modules = array('block', 'views');
protected function setUp() {
parent::setUp();
// Update admin user to have the 'administer blocks' permission.
$this->adminUser = $this->drupalCreateUser(array(
'administer content types',
'administer comments',
'skip comment approval',
'post comments',
'access comments',
'access content',
'administer blocks',
));
}
/**
* Tests the recent comments block.
*/
function testRecentCommentBlock() {
$this->drupalLogin($this->adminUser);
$block = $this->drupalPlaceBlock('views_block:comments_recent-block_1');
// Add some test comments, with and without subjects. Because the 10 newest
// comments should be shown by the block, we create 11 to test that behavior
// below.
$timestamp = REQUEST_TIME;
for ($i = 0; $i < 11; ++$i) {
$subject = ($i % 2) ? $this->randomMachineName() : '';
$comments[$i] = $this->postComment($this->node, $this->randomMachineName(), $subject);
$comments[$i]->created->value = $timestamp--;
$comments[$i]->save();
}
// Test that a user without the 'access comments' permission cannot see the
// block.
$this->drupalLogout();
user_role_revoke_permissions(RoleInterface::ANONYMOUS_ID, array('access comments'));
$this->drupalGet('');
$this->assertNoText(t('Recent comments'));
user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, array('access comments'));
// Test that a user with the 'access comments' permission can see the
// block.
$this->drupalLogin($this->webUser);
$this->drupalGet('');
$this->assertText(t('Recent comments'));
// Test the only the 10 latest comments are shown and in the proper order.
$this->assertNoText($comments[10]->getSubject(), 'Comment 11 not found in block.');
for ($i = 0; $i < 10; $i++) {
$this->assertText($comments[$i]->getSubject(), SafeMarkup::format('Comment @number found in block.', array('@number' => 10 - $i)));
if ($i > 1) {
$previous_position = $position;
$position = strpos($this->getRawContent(), $comments[$i]->getSubject());
$this->assertTrue($position > $previous_position, SafeMarkup::format('Comment @a appears after comment @b', array('@a' => 10 - $i, '@b' => 11 - $i)));
}
$position = strpos($this->getRawContent(), $comments[$i]->getSubject());
}
// Test that links to comments work when comments are across pages.
$this->setCommentsPerPage(1);
for ($i = 0; $i < 10; $i++) {
$this->clickLink($comments[$i]->getSubject());
$this->assertText($comments[$i]->getSubject(), 'Comment link goes to correct page.');
$this->assertRaw('<link rel="canonical"', 'Canonical URL was found in the HTML head');
}
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentBookTest.
*/
namespace Drupal\comment\Tests;
use Drupal\comment\CommentInterface;
use Drupal\simpletest\WebTestBase;
/**
* Tests visibility of comments on book pages.
*
* @group comment
*/
class CommentBookTest extends WebTestBase {
use CommentTestTrait;
/**
* Modules to install.
*
* @var array
*/
public static $modules = array('book', 'comment');
protected function setUp() {
parent::setUp();
// Create comment field on book.
$this->addDefaultCommentField('node', 'book');
}
/**
* Tests comments in book export.
*/
public function testBookCommentPrint() {
$book_node = entity_create('node', array(
'type' => 'book',
'title' => 'Book title',
'body' => 'Book body',
));
$book_node->book['bid'] = 'new';
$book_node->save();
$comment_subject = $this->randomMachineName(8);
$comment_body = $this->randomMachineName(8);
$comment = entity_create('comment', array(
'subject' => $comment_subject,
'comment_body' => $comment_body,
'entity_id' => $book_node->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'status' => CommentInterface::PUBLISHED,
));
$comment->save();
$commenting_user = $this->drupalCreateUser(array('access printer-friendly version', 'access comments', 'post comments'));
$this->drupalLogin($commenting_user);
$this->drupalGet('node/' . $book_node->id());
$this->assertText($comment_subject, 'Comment subject found');
$this->assertText($comment_body, 'Comment body found');
$this->assertText(t('Add new comment'), 'Comment form found');
$this->assertField('subject[0][value]', 'Comment form subject found');
$this->drupalGet('book/export/html/' . $book_node->id());
$this->assertText(t('Comments'), 'Comment thread found');
$this->assertText($comment_subject, 'Comment subject found');
$this->assertText($comment_body, 'Comment body found');
$this->assertNoText(t('Add new comment'), 'Comment form not found');
$this->assertNoField('subject[0][value]', 'Comment form subject not found');
}
}

View file

@ -0,0 +1,138 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentCSSTest.
*/
namespace Drupal\comment\Tests;
use Drupal\Core\Language\LanguageInterface;
use Drupal\comment\CommentInterface;
use Drupal\user\RoleInterface;
/**
* Tests CSS classes on comments.
*
* @group comment
*/
class CommentCSSTest extends CommentTestBase {
protected function setUp() {
parent::setUp();
// Allow anonymous users to see comments.
user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, array(
'access comments',
'access content'
));
}
/**
* Tests CSS classes on comments.
*/
function testCommentClasses() {
// Create all permutations for comments, users, and nodes.
$parameters = array(
'node_uid' => array(0, $this->webUser->id()),
'comment_uid' => array(0, $this->webUser->id(), $this->adminUser->id()),
'comment_status' => array(CommentInterface::PUBLISHED, CommentInterface::NOT_PUBLISHED),
'user' => array('anonymous', 'authenticated', 'admin'),
);
$permutations = $this->generatePermutations($parameters);
foreach ($permutations as $case) {
// Create a new node.
$node = $this->drupalCreateNode(array('type' => 'article', 'uid' => $case['node_uid']));
// Add a comment.
/** @var \Drupal\comment\CommentInterface $comment */
$comment = entity_create('comment', array(
'entity_id' => $node->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'uid' => $case['comment_uid'],
'status' => $case['comment_status'],
'subject' => $this->randomMachineName(),
'language' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'comment_body' => array(LanguageInterface::LANGCODE_NOT_SPECIFIED => array($this->randomMachineName())),
));
$comment->save();
// Adjust the current/viewing user.
switch ($case['user']) {
case 'anonymous':
if ($this->loggedInUser) {
$this->drupalLogout();
}
$case['user_uid'] = 0;
break;
case 'authenticated':
$this->drupalLogin($this->webUser);
$case['user_uid'] = $this->webUser->id();
break;
case 'admin':
$this->drupalLogin($this->adminUser);
$case['user_uid'] = $this->adminUser->id();
break;
}
// Request the node with the comment.
$this->drupalGet('node/' . $node->id());
$settings = $this->getDrupalSettings();
// Verify the data-history-node-id attribute, which is necessary for the
// by-viewer class and the "new" indicator, see below.
$this->assertIdentical(1, count($this->xpath('//*[@data-history-node-id="' . $node->id() . '"]')), 'data-history-node-id attribute is set on node.');
// Verify classes if the comment is visible for the current user.
if ($case['comment_status'] == CommentInterface::PUBLISHED || $case['user'] == 'admin') {
// Verify the by-anonymous class.
$comments = $this->xpath('//*[contains(@class, "comment") and contains(@class, "by-anonymous")]');
if ($case['comment_uid'] == 0) {
$this->assertTrue(count($comments) == 1, 'by-anonymous class found.');
}
else {
$this->assertFalse(count($comments), 'by-anonymous class not found.');
}
// Verify the by-node-author class.
$comments = $this->xpath('//*[contains(@class, "comment") and contains(@class, "by-node-author")]');
if ($case['comment_uid'] > 0 && $case['comment_uid'] == $case['node_uid']) {
$this->assertTrue(count($comments) == 1, 'by-node-author class found.');
}
else {
$this->assertFalse(count($comments), 'by-node-author class not found.');
}
// Verify the data-comment-user-id attribute, which is used by the
// drupal.comment-by-viewer library to add a by-viewer when the current
// user (the viewer) was the author of the comment. We do this in Java-
// Script to prevent breaking the render cache.
$this->assertIdentical(1, count($this->xpath('//*[contains(@class, "comment") and @data-comment-user-id="' . $case['comment_uid'] . '"]')), 'data-comment-user-id attribute is set on comment.');
$this->assertRaw(drupal_get_path('module', 'comment') . '/js/comment-by-viewer.js', 'drupal.comment-by-viewer library is present.');
}
// Verify the unpublished class.
$comments = $this->xpath('//*[contains(@class, "comment") and contains(@class, "unpublished")]');
if ($case['comment_status'] == CommentInterface::NOT_PUBLISHED && $case['user'] == 'admin') {
$this->assertTrue(count($comments) == 1, 'unpublished class found.');
}
else {
$this->assertFalse(count($comments), 'unpublished class not found.');
}
// Verify the data-comment-timestamp attribute, which is used by the
// drupal.comment-new-indicator library to add a "new" indicator to each
// comment that was created or changed after the last time the current
// user read the corresponding node.
if ($case['comment_status'] == CommentInterface::PUBLISHED || $case['user'] == 'admin') {
$this->assertIdentical(1, count($this->xpath('//*[contains(@class, "comment")]/*[@data-comment-timestamp="' . $comment->getChangedTime() . '"]')), 'data-comment-timestamp attribute is set on comment');
$expectedJS = ($case['user'] !== 'anonymous');
$this->assertIdentical($expectedJS, isset($settings['ajaxPageState']['libraries']) && in_array('comment/drupal.comment-new-indicator', explode(',', $settings['ajaxPageState']['libraries'])), 'drupal.comment-new-indicator library is present.');
}
}
}
}

View file

@ -0,0 +1,110 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentCacheTagsTest.
*/
namespace Drupal\comment\Tests;
use Drupal\comment\CommentManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\system\Tests\Entity\EntityWithUriCacheTagsTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests the Comment entity's cache tags.
*
* @group comment
*/
class CommentCacheTagsTest extends EntityWithUriCacheTagsTestBase {
use CommentTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = array('comment');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Give anonymous users permission to view comments, so that we can verify
// the cache tags of cached versions of comment pages.
$user_role = Role::load(RoleInterface::ANONYMOUS_ID);
$user_role->grantPermission('access comments');
$user_role->save();
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
// Create a "bar" bundle for the "entity_test" entity type and create.
$bundle = 'bar';
entity_test_create_bundle($bundle, NULL, 'entity_test');
// Create a comment field on this bundle.
$this->addDefaultCommentField('entity_test', 'bar', 'comment');
// Display comments in a flat list; threaded comments are not render cached.
$field = FieldConfig::loadByName('entity_test', 'bar', 'comment');
$field->setSetting('default_mode', CommentManagerInterface::COMMENT_MODE_FLAT);
$field->save();
// Create a "Camelids" test entity.
$entity_test = entity_create('entity_test', array(
'name' => 'Camelids',
'type' => 'bar',
));
$entity_test->save();
// Create a "Llama" comment.
$comment = entity_create('comment', array(
'subject' => 'Llama',
'comment_body' => array(
'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
'format' => 'plain_text',
),
'entity_id' => $entity_test->id(),
'entity_type' => 'entity_test',
'field_name' => 'comment',
'status' => \Drupal\comment\CommentInterface::PUBLISHED,
));
$comment->save();
return $comment;
}
/**
* {@inheritdoc}
*/
protected function getAdditionalCacheContextsForEntity(EntityInterface $entity) {
return [
// Field access for the user picture rendered as part of the node that
// this comment is created on.
'user.permissions',
];
}
/**
* {@inheritdoc}
*
* Each comment must have a comment body, which always has a text format.
*/
protected function getAdditionalCacheTagsForEntity(EntityInterface $entity) {
/** @var \Drupal\comment\CommentInterface $entity */
return array(
'config:filter.format.plain_text',
'user:' . $entity->getOwnerId(),
'user_view',
);
}
}

View file

@ -0,0 +1,133 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentDefaultFormatterCacheTagsTest.
*/
namespace Drupal\comment\Tests;
use Drupal\Core\Session\UserSession;
use Drupal\comment\CommentInterface;
use Drupal\system\Tests\Entity\EntityUnitTestBase;
/**
* Tests the bubbling up of comment cache tags when using the Comment list
* formatter on an entity.
*
* @group comment
*/
class CommentDefaultFormatterCacheTagsTest extends EntityUnitTestBase {
use CommentTestTrait;
/**
* Modules to install.
*
* @var array
*/
public static $modules = array('entity_test', 'comment');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Set the current user to one that can access comments. Specifically, this
// user does not have access to the 'administer comments' permission, to
// ensure only published comments are visible to the end user.
$current_user = $this->container->get('current_user');
$current_user->setAccount($this->createUser(array(), array('access comments')));
// Install tables and config needed to render comments.
$this->installSchema('comment', array('comment_entity_statistics'));
$this->installConfig(array('system', 'filter', 'comment'));
// Comment rendering generates links, so build the router.
$this->installSchema('system', array('router'));
$this->container->get('router.builder')->rebuild();
// Set up a field, so that the entity that'll be referenced bubbles up a
// cache tag when rendering it entirely.
$this->addDefaultCommentField('entity_test', 'entity_test');
}
/**
* Tests the bubbling of cache tags.
*/
public function testCacheTags() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = $this->container->get('renderer');
// Create the entity that will be commented upon.
$commented_entity = entity_create('entity_test', array('name' => $this->randomMachineName()));
$commented_entity->save();
// Verify cache tags on the rendered entity before it has comments.
$build = \Drupal::entityManager()
->getViewBuilder('entity_test')
->view($commented_entity);
$renderer->renderRoot($build);
$expected_cache_tags = array(
'entity_test_view',
'entity_test:' . $commented_entity->id(),
'comment_list',
'config:core.entity_form_display.comment.comment.default',
'config:field.field.comment.comment.comment_body',
'config:field.field.entity_test.entity_test.comment',
'config:field.storage.comment.comment_body',
'config:user.settings',
);
sort($expected_cache_tags);
$this->assertEqual($build['#cache']['tags'], $expected_cache_tags, 'The test entity has the expected cache tags before it has comments.');
// Create a comment on that entity. Comment loading requires that the uid
// also exists in the {users} table.
$user = $this->createUser();
$user->save();
$comment = entity_create('comment', array(
'subject' => 'Llama',
'comment_body' => array(
'value' => 'Llamas are cool!',
'format' => 'plain_text',
),
'entity_id' => $commented_entity->id(),
'entity_type' => 'entity_test',
'field_name' => 'comment',
'comment_type' => 'comment',
'status' => CommentInterface::PUBLISHED,
'uid' => $user->id(),
));
$comment->save();
// Load commented entity so comment_count gets computed.
// @todo Remove the $reset = TRUE parameter after
// https://www.drupal.org/node/597236 lands. It's a temporary work-around.
$commented_entity = entity_load('entity_test', $commented_entity->id(), TRUE);
// Verify cache tags on the rendered entity when it has comments.
$build = \Drupal::entityManager()
->getViewBuilder('entity_test')
->view($commented_entity);
$renderer->renderRoot($build);
$expected_cache_tags = array(
'entity_test_view',
'entity_test:' . $commented_entity->id(),
'comment_list',
'comment_view',
'comment:' . $comment->id(),
'config:filter.format.plain_text',
'user_view',
'user:2',
'config:core.entity_form_display.comment.comment.default',
'config:field.field.comment.comment.comment_body',
'config:field.field.entity_test.entity_test.comment',
'config:field.storage.comment.comment_body',
'config:user.settings',
);
sort($expected_cache_tags);
$this->assertEqual($build['#cache']['tags'], $expected_cache_tags, 'The test entity has the expected cache tags when it has comments.');
}
}

View file

@ -0,0 +1,283 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentFieldAccessTest.
*/
namespace Drupal\comment\Tests;
use Drupal\comment\Entity\Comment;
use Drupal\comment\Entity\CommentType;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\simpletest\TestBase;
use Drupal\system\Tests\Entity\EntityUnitTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests comment field level access.
*
* @group comment
* @group Access
*/
class CommentFieldAccessTest extends EntityUnitTestBase {
use CommentTestTrait;
/**
* Modules to install.
*
* @var array
*/
public static $modules = array('comment', 'entity_test', 'user');
/**
* Fields that only users with administer comments permissions can change.
*
* @var array
*/
protected $administrativeFields = array(
'uid',
'status',
'created',
);
/**
* These fields are automatically managed and can not be changed by any user.
*
* @var array
*/
protected $readOnlyFields = array(
'changed',
'hostname',
'uuid',
'cid',
'thread',
'comment_type',
'pid',
'entity_id',
'entity_type',
'field_name',
);
/**
* These fields can only be edited by the admin or anonymous users if allowed.
*
* @var array
*/
protected $contactFields = array(
'name',
'mail',
'homepage',
);
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installConfig(array('user', 'comment'));
$this->installSchema('comment', array('comment_entity_statistics'));
}
/**
* Test permissions on comment fields.
*/
public function testAccessToAdministrativeFields() {
// Create a comment type.
$comment_type = CommentType::create([
'id' => 'comment',
'label' => 'Default comments',
'description' => 'Default comment field',
'target_entity_type_id' => 'entity_test',
]);
$comment_type->save();
// Create a comment against a test entity.
$host = EntityTest::create();
$host->save();
// An administrator user. No user exists yet, ensure that the first user
// does not have UID 1.
$comment_admin_user = $this->createUser(['uid' => 2, 'name' => 'admin'], [
'administer comments',
'access comments',
]);
// Two comment enabled users, one with edit access.
$comment_enabled_user = $this->createUser(['name' => 'enabled'], [
'post comments',
'skip comment approval',
'edit own comments',
'access comments',
]);
$comment_no_edit_user = $this->createUser(['name' => 'no edit'], [
'post comments',
'skip comment approval',
'access comments',
]);
// An unprivileged user.
$comment_disabled_user = $this->createUser(['name' => 'disabled'], ['access content']);
$role = Role::load(RoleInterface::ANONYMOUS_ID);
$role->grantPermission('post comments')
->save();
$anonymous_user = new AnonymousUserSession();
// Add two fields.
$this->addDefaultCommentField('entity_test', 'entity_test', 'comment');
$this->addDefaultCommentField('entity_test', 'entity_test', 'comment_other');
// Change the second field's anonymous contact setting.
$instance = FieldConfig::loadByName('entity_test', 'entity_test', 'comment_other');
// Default is 'May not contact', for this field - they may contact.
$instance->setSetting('anonymous', COMMENT_ANONYMOUS_MAY_CONTACT);
$instance->save();
// Create three "Comments". One is owned by our edit-enabled user.
$comment1 = Comment::create([
'entity_type' => 'entity_test',
'name' => 'Tony',
'hostname' => 'magic.example.com',
'mail' => 'tonythemagicalpony@example.com',
'subject' => 'Bruce the Mesopotamian moose',
'entity_id' => $host->id(),
'comment_type' => 'comment',
'field_name' => 'comment',
'pid' => 0,
'uid' => 0,
'status' => 1,
]);
$comment1->save();
$comment2 = Comment::create([
'entity_type' => 'entity_test',
'hostname' => 'magic.example.com',
'subject' => 'Brian the messed up lion',
'entity_id' => $host->id(),
'comment_type' => 'comment',
'field_name' => 'comment',
'status' => 1,
'pid' => 0,
'uid' => $comment_enabled_user->id(),
]);
$comment2->save();
$comment3 = Comment::create([
'entity_type' => 'entity_test',
'hostname' => 'magic.example.com',
// Unpublished.
'status' => 0,
'subject' => 'Gail the minky whale',
'entity_id' => $host->id(),
'comment_type' => 'comment',
'field_name' => 'comment_other',
'pid' => $comment2->id(),
'uid' => $comment_no_edit_user->id(),
]);
$comment3->save();
// Note we intentionally don't save this comment so it remains 'new'.
$comment4 = Comment::create([
'entity_type' => 'entity_test',
'hostname' => 'magic.example.com',
// Unpublished.
'status' => 0,
'subject' => 'Daniel the Cocker-Spaniel',
'entity_id' => $host->id(),
'comment_type' => 'comment',
'field_name' => 'comment_other',
'pid' => 0,
'uid' => $anonymous_user->id(),
]);
// Generate permutations.
$combinations = [
'comment' => [$comment1, $comment2, $comment3, $comment4],
'user' => [$comment_admin_user, $comment_enabled_user, $comment_no_edit_user, $comment_disabled_user, $anonymous_user]
];
$permutations = TestBase::generatePermutations($combinations);
// Check access to administrative fields.
foreach ($this->administrativeFields as $field) {
foreach ($permutations as $set) {
$may_view = $set['comment']->{$field}->access('view', $set['user']);
$may_update = $set['comment']->{$field}->access('edit', $set['user']);
$this->assertEqual($may_view, $set['user']->hasPermission('administer comments') || ($set['comment']->isPublished() && $set['user']->hasPermission('access comments')), SafeMarkup::format('User @user !state view field !field on comment @comment', [
'@user' => $set['user']->getUsername(),
'!state' => $may_update ? 'can' : 'cannot',
'@comment' => $set['comment']->getSubject(),
'!field' => $field,
]));
$this->assertEqual($may_update, $set['user']->hasPermission('administer comments'), SafeMarkup::format('User @user !state update field !field on comment @comment', [
'@user' => $set['user']->getUsername(),
'!state' => $may_update ? 'can' : 'cannot',
'@comment' => $set['comment']->getSubject(),
'!field' => $field,
]));
}
}
// Check access to normal field.
foreach ($permutations as $set) {
$may_update = $set['comment']->access('update', $set['user']) && $set['comment']->subject->access('edit', $set['user']);
$this->assertEqual($may_update, $set['user']->hasPermission('administer comments') || ($set['user']->hasPermission('edit own comments') && $set['user']->id() == $set['comment']->getOwnerId()), SafeMarkup::format('User @user !state update field subject on comment @comment', [
'@user' => $set['user']->getUsername(),
'!state' => $may_update ? 'can' : 'cannot',
'@comment' => $set['comment']->getSubject(),
]));
}
// Check read-only fields.
foreach ($this->readOnlyFields as $field) {
// Check view operation.
foreach ($permutations as $set) {
$may_view = $set['comment']->{$field}->access('view', $set['user']);
$may_update = $set['comment']->{$field}->access('edit', $set['user']);
$this->assertEqual($may_view, $field != 'hostname' && ($set['user']->hasPermission('administer comments') ||
($set['comment']->isPublished() && $set['user']->hasPermission('access comments'))), SafeMarkup::format('User @user !state view field !field on comment @comment', [
'@user' => $set['user']->getUsername(),
'!state' => $may_view ? 'can' : 'cannot',
'@comment' => $set['comment']->getSubject(),
'!field' => $field,
]));
$this->assertFalse($may_update, SafeMarkup::format('User @user !state update field !field on comment @comment', [
'@user' => $set['user']->getUsername(),
'!state' => $may_update ? 'can' : 'cannot',
'@comment' => $set['comment']->getSubject(),
'!field' => $field,
]));
}
}
// Check contact fields.
foreach ($this->contactFields as $field) {
// Check view operation.
foreach ($permutations as $set) {
$may_update = $set['comment']->{$field}->access('edit', $set['user']);
// To edit the 'mail' or 'name' field, either the user has the
// "administer comments" permissions or the user is anonymous and
// adding a new comment using a field that allows contact details.
$this->assertEqual($may_update, $set['user']->hasPermission('administer comments') || (
$set['user']->isAnonymous() &&
$set['comment']->isNew() &&
$set['user']->hasPermission('post comments') &&
$set['comment']->getFieldName() == 'comment_other'
), SafeMarkup::format('User @user !state update field !field on comment @comment', [
'@user' => $set['user']->getUsername(),
'!state' => $may_update ? 'can' : 'cannot',
'@comment' => $set['comment']->getSubject(),
'!field' => $field,
]));
}
}
foreach ($permutations as $set) {
// Check no view-access to mail field for other than admin.
$may_view = $set['comment']->mail->access('view', $set['user']);
$this->assertEqual($may_view, $set['user']->hasPermission('administer comments'));
}
}
}

View file

@ -0,0 +1,191 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentFieldsTest.
*/
namespace Drupal\comment\Tests;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\comment\Entity\CommentType;
/**
* Tests fields on comments.
*
* @group comment
*/
class CommentFieldsTest extends CommentTestBase {
/**
* Install the field UI.
*
* @var array
*/
public static $modules = array('field_ui');
/**
* Tests that the default 'comment_body' field is correctly added.
*/
function testCommentDefaultFields() {
// Do not make assumptions on default node types created by the test
// installation profile, and create our own.
$this->drupalCreateContentType(array('type' => 'test_node_type'));
$this->addDefaultCommentField('node', 'test_node_type');
// Check that the 'comment_body' field is present on the comment bundle.
$field = FieldConfig::loadByName('comment', 'comment', 'comment_body');
$this->assertTrue(!empty($field), 'The comment_body field is added when a comment bundle is created');
$field->delete();
// Check that the 'comment_body' field is not deleted since it is persisted
// even if it has no fields.
$field_storage = FieldStorageConfig::loadByName('comment', 'comment_body');
$this->assertTrue($field_storage, 'The comment_body field storage was not deleted');
// Create a new content type.
$type_name = 'test_node_type_2';
$this->drupalCreateContentType(array('type' => $type_name));
$this->addDefaultCommentField('node', $type_name);
// Check that the 'comment_body' field exists and has an instance on the
// new comment bundle.
$field_storage = FieldStorageConfig::loadByName('comment', 'comment_body');
$this->assertTrue($field_storage, 'The comment_body field exists');
$field = FieldConfig::loadByName('comment', 'comment', 'comment_body');
$this->assertTrue(isset($field), format_string('The comment_body field is present for comments on type @type', array('@type' => $type_name)));
// Test adding a field that defaults to CommentItemInterface::CLOSED.
$this->addDefaultCommentField('node', 'test_node_type', 'who_likes_ponies', CommentItemInterface::CLOSED, 'who_likes_ponies');
$field = FieldConfig::load('node.test_node_type.who_likes_ponies');
$this->assertEqual($field->default_value[0]['status'], CommentItemInterface::CLOSED);
}
/**
* Tests that you can remove a comment field.
*/
public function testCommentFieldDelete() {
$this->drupalCreateContentType(array('type' => 'test_node_type'));
$this->addDefaultCommentField('node', 'test_node_type');
// We want to test the handling of removing the primary comment field, so we
// ensure there is at least one other comment field attached to a node type
// so that comment_entity_load() runs for nodes.
$this->addDefaultCommentField('node', 'test_node_type', 'comment2');
// Create a sample node.
$node = $this->drupalCreateNode(array(
'title' => 'Baloney',
'type' => 'test_node_type',
));
$this->drupalLogin($this->webUser);
$this->drupalGet('node/' . $node->nid->value);
$elements = $this->cssSelect('.field-type-comment');
$this->assertEqual(2, count($elements), 'There are two comment fields on the node.');
// Delete the first comment field.
FieldStorageConfig::loadByName('node', 'comment')->delete();
$this->drupalGet('node/' . $node->nid->value);
$elements = $this->cssSelect('.field-type-comment');
$this->assertEqual(1, count($elements), 'There is one comment field on the node.');
}
/**
* Tests creating a comment field through the interface.
*/
public function testCommentFieldCreate() {
// Create user who can administer user fields.
$user = $this->drupalCreateUser(array(
'administer user fields',
));
$this->drupalLogin($user);
// Create comment field in account settings.
$edit = array(
'new_storage_type' => 'comment',
'label' => 'User comment',
'field_name' => 'user_comment',
);
$this->drupalPostForm('admin/config/people/accounts/fields/add-field', $edit, 'Save and continue');
// Try to save the comment field without selecting a comment type.
$edit = array();
$this->drupalPostForm('admin/config/people/accounts/fields/user.user.field_user_comment/storage', $edit, t('Save field settings'));
// We should get an error message.
$this->assertText(t('An illegal choice has been detected. Please contact the site administrator.'));
// Create a comment type for users.
$bundle = CommentType::create(array(
'id' => 'user_comment_type',
'label' => 'user_comment_type',
'description' => '',
'target_entity_type_id' => 'user',
));
$bundle->save();
// Select a comment type and try to save again.
$edit = array(
'settings[comment_type]' => 'user_comment_type',
);
$this->drupalPostForm('admin/config/people/accounts/fields/user.user.field_user_comment/storage', $edit, t('Save field settings'));
// We shouldn't get an error message.
$this->assertNoText(t('An illegal choice has been detected. Please contact the site administrator.'));
}
/**
* Tests that comment module works when installed after a content module.
*/
function testCommentInstallAfterContentModule() {
// Create a user to do module administration.
$this->adminUser = $this->drupalCreateUser(array('access administration pages', 'administer modules'));
$this->drupalLogin($this->adminUser);
// Drop default comment field added in CommentTestBase::setup().
FieldStorageConfig::loadByName('node', 'comment')->delete();
if ($field_storage = FieldStorageConfig::loadByName('node', 'comment_forum')) {
$field_storage->delete();
}
// Purge field data now to allow comment module to be uninstalled once the
// field has been deleted.
field_purge_batch(10);
// Uninstall the comment module.
$edit = array();
$edit['uninstall[comment]'] = TRUE;
$this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
$this->drupalPostForm(NULL, array(), t('Uninstall'));
$this->rebuildContainer();
$this->assertFalse($this->container->get('module_handler')->moduleExists('comment'), 'Comment module uninstalled.');
// Install core content type module (book).
$edit = array();
$edit['modules[Core][book][enable]'] = 'book';
$this->drupalPostForm('admin/modules', $edit, t('Save configuration'));
// Now install the comment module.
$edit = array();
$edit['modules[Core][comment][enable]'] = 'comment';
$this->drupalPostForm('admin/modules', $edit, t('Save configuration'));
$this->rebuildContainer();
$this->assertTrue($this->container->get('module_handler')->moduleExists('comment'), 'Comment module enabled.');
// Create nodes of each type.
$this->addDefaultCommentField('node', 'book');
$book_node = $this->drupalCreateNode(array('type' => 'book'));
$this->drupalLogout();
// Try to post a comment on each node. A failure will be triggered if the
// comment body is missing on one of these forms, due to postComment()
// asserting that the body is actually posted correctly.
$this->webUser = $this->drupalCreateUser(array('access content', 'access comments', 'post comments', 'skip comment approval'));
$this->drupalLogin($this->webUser);
$this->postComment($book_node, $this->randomMachineName(), $this->randomMachineName());
}
}

View file

@ -0,0 +1,284 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentInterfaceTest.
*/
namespace Drupal\comment\Tests;
use Drupal\comment\CommentManagerInterface;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\comment\Entity\Comment;
use Drupal\user\RoleInterface;
use Drupal\filter\Entity\FilterFormat;
/**
* Tests comment user interfaces.
*
* @group comment
*/
class CommentInterfaceTest extends CommentTestBase {
/**
* Set up comments to have subject and preview disabled.
*/
public function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
$this->setCommentPreview(DRUPAL_DISABLED);
$this->setCommentForm(TRUE);
$this->setCommentSubject(FALSE);
$this->setCommentSettings('default_mode', CommentManagerInterface::COMMENT_MODE_THREADED, 'Comment paging changed.');
$this->drupalLogout();
}
/**
* Tests the comment interface.
*/
public function testCommentInterface() {
// Post comment #1 without subject or preview.
$this->drupalLogin($this->webUser);
$comment_text = $this->randomMachineName();
$comment = $this->postComment($this->node, $comment_text);
$this->assertTrue($this->commentExists($comment), 'Comment found.');
// Set comments to have subject and preview to required.
$this->drupalLogout();
$this->drupalLogin($this->adminUser);
$this->setCommentSubject(TRUE);
$this->setCommentPreview(DRUPAL_REQUIRED);
$this->drupalLogout();
// Create comment #2 that allows subject and requires preview.
$this->drupalLogin($this->webUser);
$subject_text = $this->randomMachineName();
$comment_text = $this->randomMachineName();
$comment = $this->postComment($this->node, $comment_text, $subject_text, TRUE);
$this->assertTrue($this->commentExists($comment), 'Comment found.');
// Comment as anonymous with preview required.
$this->drupalLogout();
user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, array('access content', 'access comments', 'post comments', 'skip comment approval'));
$anonymous_comment = $this->postComment($this->node, $this->randomMachineName(), $this->randomMachineName(), TRUE);
$this->assertTrue($this->commentExists($anonymous_comment), 'Comment found.');
$anonymous_comment->delete();
// Check comment display.
$this->drupalLogin($this->webUser);
$this->drupalGet('node/' . $this->node->id());
$this->assertText($subject_text, 'Individual comment subject found.');
$this->assertText($comment_text, 'Individual comment body found.');
// Set comments to have subject and preview to optional.
$this->drupalLogout();
$this->drupalLogin($this->adminUser);
$this->setCommentSubject(TRUE);
$this->setCommentPreview(DRUPAL_OPTIONAL);
$this->drupalGet('comment/' . $comment->id() . '/edit');
$this->assertTitle(t('Edit comment @title | Drupal', array(
'@title' => $comment->getSubject(),
)));
// Test changing the comment author to "Anonymous".
$comment = $this->postComment(NULL, $comment->comment_body->value, $comment->getSubject(), array('name' => ''));
$this->assertTrue($comment->getAuthorName() == t('Anonymous') && $comment->getOwnerId() == 0, 'Comment author successfully changed to anonymous.');
// Test changing the comment author to an unverified user.
$random_name = $this->randomMachineName();
$this->drupalGet('comment/' . $comment->id() . '/edit');
$comment = $this->postComment(NULL, $comment->comment_body->value, $comment->getSubject(), array('name' => $random_name));
$this->drupalGet('node/' . $this->node->id());
$this->assertText($random_name . ' (' . t('not verified') . ')', 'Comment author successfully changed to an unverified user.');
// Test changing the comment author to a verified user.
$this->drupalGet('comment/' . $comment->id() . '/edit');
$comment = $this->postComment(NULL, $comment->comment_body->value, $comment->getSubject(), array('name' => $this->webUser->getUsername()));
$this->assertTrue($comment->getAuthorName() == $this->webUser->getUsername() && $comment->getOwnerId() == $this->webUser->id(), 'Comment author successfully changed to a registered user.');
$this->drupalLogout();
// Reply to comment #2 creating comment #3 with optional preview and no
// subject though field enabled.
$this->drupalLogin($this->webUser);
// Deliberately use the wrong url to test
// \Drupal\comment\Controller\CommentController::redirectNode().
$this->drupalGet('comment/' . $this->node->id() . '/reply');
// Verify we were correctly redirected.
$this->assertUrl(\Drupal::url('comment.reply', array('entity_type' => 'node', 'entity' => $this->node->id(), 'field_name' => 'comment'), array('absolute' => TRUE)));
$this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment/' . $comment->id());
$this->assertText($subject_text, 'Individual comment-reply subject found.');
$this->assertText($comment_text, 'Individual comment-reply body found.');
$reply = $this->postComment(NULL, $this->randomMachineName(), '', TRUE);
$reply_loaded = Comment::load($reply->id());
$this->assertTrue($this->commentExists($reply, TRUE), 'Reply found.');
$this->assertEqual($comment->id(), $reply_loaded->getParentComment()->id(), 'Pid of a reply to a comment is set correctly.');
// Check the thread of reply grows correctly.
$this->assertEqual(rtrim($comment->getThread(), '/') . '.00/', $reply_loaded->getThread());
// Second reply to comment #2 creating comment #4.
$this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment/' . $comment->id());
$this->assertText($comment->getSubject(), 'Individual comment-reply subject found.');
$this->assertText($comment->comment_body->value, 'Individual comment-reply body found.');
$reply = $this->postComment(NULL, $this->randomMachineName(), $this->randomMachineName(), TRUE);
$reply_loaded = Comment::load($reply->id());
$this->assertTrue($this->commentExists($reply, TRUE), 'Second reply found.');
// Check the thread of second reply grows correctly.
$this->assertEqual(rtrim($comment->getThread(), '/') . '.01/', $reply_loaded->getThread());
// Reply to comment #4 creating comment #5.
$this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment/' . $reply_loaded->id());
$this->assertText($reply_loaded->getSubject(), 'Individual comment-reply subject found.');
$this->assertText($reply_loaded->comment_body->value, 'Individual comment-reply body found.');
$reply = $this->postComment(NULL, $this->randomMachineName(), $this->randomMachineName(), TRUE);
$reply_loaded = Comment::load($reply->id());
$this->assertTrue($this->commentExists($reply, TRUE), 'Second reply found.');
// Check the thread of reply to second reply grows correctly.
$this->assertEqual(rtrim($comment->getThread(), '/') . '.01.00/', $reply_loaded->getThread());
// Edit reply.
$this->drupalGet('comment/' . $reply->id() . '/edit');
$reply = $this->postComment(NULL, $this->randomMachineName(), $this->randomMachineName(), TRUE);
$this->assertTrue($this->commentExists($reply, TRUE), 'Modified reply found.');
// Confirm a new comment is posted to the correct page.
$this->setCommentsPerPage(2);
$comment_new_page = $this->postComment($this->node, $this->randomMachineName(), $this->randomMachineName(), TRUE);
$this->assertTrue($this->commentExists($comment_new_page), 'Page one exists. %s');
$this->drupalGet('node/' . $this->node->id(), array('query' => array('page' => 2)));
$this->assertTrue($this->commentExists($reply, TRUE), 'Page two exists. %s');
$this->setCommentsPerPage(50);
// Attempt to reply to an unpublished comment.
$reply_loaded->setPublished(FALSE);
$reply_loaded->save();
$this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment/' . $reply_loaded->id());
$this->assertResponse(403);
// Attempt to post to node with comments disabled.
$this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'comment' => array(array('status' => CommentItemInterface::HIDDEN))));
$this->assertTrue($this->node, 'Article node created.');
$this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment');
$this->assertResponse(403);
$this->assertNoField('edit-comment', 'Comment body field found.');
// Attempt to post to node with read-only comments.
$this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'comment' => array(array('status' => CommentItemInterface::CLOSED))));
$this->assertTrue($this->node, 'Article node created.');
$this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment');
$this->assertResponse(403);
$this->assertNoField('edit-comment', 'Comment body field found.');
// Attempt to post to node with comments enabled (check field names etc).
$this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'comment' => array(array('status' => CommentItemInterface::OPEN))));
$this->assertTrue($this->node, 'Article node created.');
$this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment');
$this->assertNoText('This discussion is closed', 'Posting to node with comments enabled');
$this->assertField('edit-comment-body-0-value', 'Comment body field found.');
// Delete comment and make sure that reply is also removed.
$this->drupalLogout();
$this->drupalLogin($this->adminUser);
$this->deleteComment($comment);
$this->deleteComment($comment_new_page);
$this->drupalGet('node/' . $this->node->id());
$this->assertFalse($this->commentExists($comment), 'Comment not found.');
$this->assertFalse($this->commentExists($reply, TRUE), 'Reply not found.');
// Enabled comment form on node page.
$this->drupalLogin($this->adminUser);
$this->setCommentForm(TRUE);
$this->drupalLogout();
// Submit comment through node form.
$this->drupalLogin($this->webUser);
$this->drupalGet('node/' . $this->node->id());
$form_comment = $this->postComment(NULL, $this->randomMachineName(), $this->randomMachineName(), TRUE);
$this->assertTrue($this->commentExists($form_comment), 'Form comment found.');
// Disable comment form on node page.
$this->drupalLogout();
$this->drupalLogin($this->adminUser);
$this->setCommentForm(FALSE);
}
/**
* Test that the subject is automatically filled if disabled or left blank.
*
* When the subject field is blank or disabled, the first 29 characters of the
* comment body are used for the subject. If this would break within a word,
* then the break is put at the previous word boundary instead.
*/
public function testAutoFilledSubject() {
$this->drupalLogin($this->webUser);
$this->drupalGet('node/' . $this->node->id());
// Break when there is a word boundary before 29 characters.
$body_text = 'Lorem ipsum Lorem ipsum Loreming ipsum Lorem ipsum';
$comment1 = $this->postComment(NULL, $body_text, '', TRUE);
$this->assertTrue($this->commentExists($comment1), 'Form comment found.');
$this->assertEqual('Lorem ipsum Lorem ipsum…', $comment1->getSubject());
// Break at 29 characters where there's no boundary before that.
$body_text2 = 'LoremipsumloremipsumLoremingipsumLoremipsum';
$comment2 = $this->postComment(NULL, $body_text2, '', TRUE);
$this->assertEqual('LoremipsumloremipsumLoreming…', $comment2->getSubject());
}
/**
* Test that automatic subject is correctly created from HTML comment text.
*
* This is the same test as in CommentInterfaceTest::testAutoFilledSubject()
* with the additional check that HTML is stripped appropriately prior to
* character-counting.
*/
public function testAutoFilledHtmlSubject() {
// Set up two default (i.e. filtered HTML) input formats, because then we
// can select one of them. Then create a user that can use these formats,
// log the user in, and then GET the node page on which to test the
// comments.
$filtered_html_format = FilterFormat::create(array(
'format' => 'filtered_html',
'name' => 'Filtered HTML',
));
$filtered_html_format->save();
$full_html_format = FilterFormat::create(array(
'format' => 'full_html',
'name' => 'Full HTML',
));
$full_html_format->save();
$html_user = $this->drupalCreateUser(array(
'access comments',
'post comments',
'edit own comments',
'skip comment approval',
'access content',
$filtered_html_format->getPermissionName(),
$full_html_format->getPermissionName(),
));
$this->drupalLogin($html_user);
$this->drupalGet('node/' . $this->node->id());
// HTML should not be included in the character count.
$body_text1 = '<span></span><strong> </strong><span> </span><strong></strong>Hello World<br />';
$edit1 = array(
'comment_body[0][value]' => $body_text1,
'comment_body[0][format]' => 'filtered_html',
);
$this->drupalPostForm(NULL, $edit1, t('Save'));
$this->assertEqual('Hello World', Comment::load(1)->getSubject());
// If there's nothing other than HTML, the subject should be '(No subject)'.
$body_text2 = '<span></span><strong> </strong><span> </span><strong></strong> <br />';
$edit2 = array(
'comment_body[0][value]' => $body_text2,
'comment_body[0][format]' => 'filtered_html',
);
$this->drupalPostForm(NULL, $edit2, t('Save'));
$this->assertEqual('(No subject)', Comment::load(2)->getSubject());
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentItemTest.
*/
namespace Drupal\comment\Tests;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\field\Tests\FieldUnitTestBase;
/**
* Tests the new entity API for the comment field type.
*
* @group comment
*/
class CommentItemTest extends FieldUnitTestBase {
use CommentTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['comment', 'entity_test', 'user'];
protected function setUp() {
parent::setUp();
$this->installSchema('comment', ['comment_entity_statistics']);
$this->installConfig(['comment']);
}
/**
* Tests using entity fields of the comment field type.
*/
public function testCommentItem() {
$this->addDefaultCommentField('entity_test', 'entity_test', 'comment');
// Verify entity creation.
$entity = entity_create('entity_test');
$entity->name->value = $this->randomMachineName();
$entity->save();
// Verify entity has been created properly.
$id = $entity->id();
$entity = entity_load('entity_test', $id, TRUE);
$this->assertTrue($entity->comment instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->comment[0] instanceof CommentItemInterface, 'Field item implements interface.');
// Test sample item generation.
/** @var \Drupal\entity_test\Entity\EntityTest $entity */
$entity = entity_create('entity_test');
$entity->comment->generateSampleItems();
$this->entityValidateAndSave($entity);
$this->assertTrue(in_array($entity->get('comment')->status, [
CommentItemInterface::HIDDEN,
CommentItemInterface::CLOSED,
CommentItemInterface::OPEN,
]), 'Comment status value in defined range');
}
}

View file

@ -0,0 +1,140 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentLanguageTest.
*/
namespace Drupal\comment\Tests;
use Drupal\comment\Entity\Comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\simpletest\WebTestBase;
/**
* Tests for comment language.
*
* @group comment
*/
class CommentLanguageTest extends WebTestBase {
use CommentTestTrait;
/**
* Modules to install.
*
* We also use the language_test module here to be able to turn on content
* language negotiation. Drupal core does not provide a way in itself to do
* that.
*
* @var array
*/
public static $modules = array('node', 'language', 'language_test', 'comment_test');
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
// Create and login user.
$admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'administer content types', 'administer comments', 'create article content', 'access comments', 'post comments', 'skip comment approval'));
$this->drupalLogin($admin_user);
// Add language.
$edit = array('predefined_langcode' => 'fr');
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
// Set "Article" content type to use multilingual support.
$edit = array('language_configuration[language_alterable]' => TRUE);
$this->drupalPostForm('admin/structure/types/manage/article', $edit, t('Save content type'));
// Enable content language negotiation UI.
\Drupal::state()->set('language_test.content_language_type', TRUE);
// Set interface language detection to user and content language detection
// to URL. Disable inheritance from interface language to ensure content
// language will fall back to the default language if no URL language can be
// detected.
$edit = array(
'language_interface[enabled][language-user]' => TRUE,
'language_content[enabled][language-url]' => TRUE,
'language_content[enabled][language-interface]' => FALSE,
);
$this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
// Change user language preference, this way interface language is always
// French no matter what path prefix the URLs have.
$edit = array('preferred_langcode' => 'fr');
$this->drupalPostForm("user/" . $admin_user->id() . "/edit", $edit, t('Save'));
// Create comment field on article.
$this->addDefaultCommentField('node', 'article');
// Make comment body translatable.
$field_storage = FieldStorageConfig::loadByName('comment', 'comment_body');
$field_storage->setTranslatable(TRUE);
$field_storage->save();
$this->assertTrue($field_storage->isTranslatable(), 'Comment body is translatable.');
}
/**
* Test that comment language is properly set.
*/
function testCommentLanguage() {
// Create two nodes, one for english and one for french, and comment each
// node using both english and french as content language by changing URL
// language prefixes. Meanwhile interface language is always French, which
// is the user language preference. This way we can ensure that node
// language and interface language do not influence comment language, as
// only content language has to.
foreach ($this->container->get('language_manager')->getLanguages() as $node_langcode => $node_language) {
// Create "Article" content.
$title = $this->randomMachineName();
$edit = array(
'title[0][value]' => $title,
'body[0][value]' => $this->randomMachineName(),
'langcode[0][value]' => $node_langcode,
'comment[0][status]' => CommentItemInterface::OPEN,
);
$this->drupalPostForm("node/add/article", $edit, t('Save'));
$node = $this->drupalGetNodeByTitle($title);
$prefixes = language_negotiation_url_prefixes();
foreach ($this->container->get('language_manager')->getLanguages() as $langcode => $language) {
// Post a comment with content language $langcode.
$prefix = empty($prefixes[$langcode]) ? '' : $prefixes[$langcode] . '/';
$comment_values[$node_langcode][$langcode] = $this->randomMachineName();
$edit = array(
'subject[0][value]' => $this->randomMachineName(),
'comment_body[0][value]' => $comment_values[$node_langcode][$langcode],
);
$this->drupalPostForm($prefix . 'node/' . $node->id(), $edit, t('Preview'));
$this->drupalPostForm(NULL, $edit, t('Save'));
// Check that comment language matches the current content language.
$cids = \Drupal::entityQuery('comment')
->condition('entity_id', $node->id())
->condition('entity_type', 'node')
->condition('field_name', 'comment')
->sort('cid', 'DESC')
->range(0, 1)
->execute();
$comment = Comment::load(reset($cids));
$args = array('%node_language' => $node_langcode, '%comment_language' => $comment->langcode->value, '%langcode' => $langcode);
$this->assertEqual($comment->langcode->value, $langcode, format_string('The comment posted with content language %langcode and belonging to the node with language %node_language has language %comment_language', $args));
$this->assertEqual($comment->comment_body->value, $comment_values[$node_langcode][$langcode], 'Comment body correctly stored.');
}
}
// Check that comment bodies appear in the administration UI.
$this->drupalGet('admin/content/comment');
foreach ($comment_values as $node_values) {
foreach ($node_values as $value) {
$this->assertRaw($value);
}
}
}
}

View file

@ -0,0 +1,40 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentLinksAlterTest.
*/
namespace Drupal\comment\Tests;
/**
* Tests comment links altering.
*
* @group comment
*/
class CommentLinksAlterTest extends CommentTestBase {
public static $modules = array('comment_test');
protected function setUp() {
parent::setUp();
// Enable comment_test.module's hook_comment_links_alter() implementation.
$this->container->get('state')->set('comment_test_links_alter_enabled', TRUE);
}
/**
* Tests comment links altering.
*/
public function testCommentLinksAlter() {
$this->drupalLogin($this->webUser);
$comment_text = $this->randomMachineName();
$subject = $this->randomMachineName();
$comment = $this->postComment($this->node, $comment_text, $subject);
$this->drupalGet('node/' . $this->node->id());
$this->assertLink(t('Report'));
}
}

View file

@ -0,0 +1,130 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentLinksTest.
*/
namespace Drupal\comment\Tests;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\comment\CommentInterface;
use Drupal\user\RoleInterface;
/**
* Basic comment links tests to ensure markup present.
*
* @group comment
*/
class CommentLinksTest extends CommentTestBase {
/**
* Comment being tested.
*
* @var \Drupal\comment\CommentInterface
*/
protected $comment;
/**
* Seen comments, array of comment IDs.
*
* @var array
*/
protected $seen = array();
/**
* Use the main node listing to test rendering on teasers.
*
* @var array
*
* @todo Remove this dependency.
*/
public static $modules = array('views');
/**
* Tests that comment links are output and can be hidden.
*/
public function testCommentLinks() {
// Bartik theme alters comment links, so use a different theme.
\Drupal::service('theme_handler')->install(array('stark'));
$this->config('system.theme')
->set('default', 'stark')
->save();
// Remove additional user permissions from $this->webUser added by setUp(),
// since this test is limited to anonymous and authenticated roles only.
$roles = $this->webUser->getRoles();
entity_delete_multiple('user_role', array(reset($roles)));
// Create a comment via CRUD API functionality, since
// $this->postComment() relies on actual user permissions.
$comment = entity_create('comment', array(
'cid' => NULL,
'entity_id' => $this->node->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'pid' => 0,
'uid' => 0,
'status' => CommentInterface::PUBLISHED,
'subject' => $this->randomMachineName(),
'hostname' => '127.0.0.1',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'comment_body' => array(LanguageInterface::LANGCODE_NOT_SPECIFIED => array($this->randomMachineName())),
));
$comment->save();
$this->comment = $comment;
// Change comment settings.
$this->setCommentSettings('form_location', CommentItemInterface::FORM_BELOW, 'Set comment form location');
$this->setCommentAnonymous(TRUE);
$this->node->comment = CommentItemInterface::OPEN;
$this->node->save();
// Change user permissions.
$perms = array(
'access comments' => 1,
'post comments' => 1,
'skip comment approval' => 1,
'edit own comments' => 1,
);
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, $perms);
$nid = $this->node->id();
// Assert basic link is output, actual functionality is unit-tested in
// \Drupal\comment\Tests\CommentLinkBuilderTest.
foreach (array('node', "node/$nid") as $path) {
$this->drupalGet($path);
// In teaser view, a link containing the comment count is always
// expected.
if ($path == 'node') {
$this->assertLink(t('1 comment'));
}
$this->assertLink('Add new comment');
}
// Make sure we can hide node links.
entity_get_display('node', $this->node->bundle(), 'default')
->removeComponent('links')
->save();
$this->drupalGet($this->node->urlInfo());
$this->assertNoLink('1 comment');
$this->assertNoLink('Add new comment');
// Visit the full node, make sure there are links for the comment.
$this->drupalGet('node/' . $this->node->id());
$this->assertText($comment->getSubject());
$this->assertLink('Reply');
// Make sure we can hide comment links.
entity_get_display('comment', 'comment', 'default')
->removeComponent('links')
->save();
$this->drupalGet('node/' . $this->node->id());
$this->assertText($comment->getSubject());
$this->assertNoLink('Reply');
}
}

View file

@ -0,0 +1,154 @@
<?php
/**
* @file
* Contains \Drupal\comment\Tests\CommentNewIndicatorTest.
*/
namespace Drupal\comment\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Language\LanguageInterface;
use Drupal\comment\CommentInterface;
use Drupal\Core\Url;
/**
* Tests the 'new' indicator posted on comments.
*
* @group comment
*/
class CommentNewIndicatorTest extends CommentTestBase {
/**
* Use the main node listing to test rendering on teasers.
*
* @var array
*
* @todo Remove this dependency.
*/
public static $modules = array('views');
/**
* Get node "x new comments" metadata from the server for the current user.
*
* @param array $node_ids
* An array of node IDs.
*
* @return string
* The response body.
*/
protected function renderNewCommentsNodeLinks(array $node_ids) {
// Build POST values.
$post = array();
for ($i = 0; $i < count($node_ids); $i++) {
$post['node_ids[' . $i . ']'] = $node_ids[$i];
}
$post['field_name'] = 'comment';
// Serialize POST values.
foreach ($post as $key => $value) {
// Encode according to application/x-www-form-urlencoded
// Both names and values needs to be urlencoded, according to
// http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
$post[$key] = urlencode($key) . '=' . urlencode($value);
}
$post = implode('&', $post);
// Perform HTTP request.
return $this->curlExec(array(
CURLOPT_URL => \Drupal::url('comment.new_comments_node_links', array(), array('absolute' => TRUE)),
CURLOPT_POST => TRUE,
CURLOPT_POSTFIELDS => $post,
CURLOPT_HTTPHEADER => array(
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded',
),
));
}
/**
* Tests new comment marker.
*/
public function testCommentNewCommentsIndicator() {
// Test if the right links are displayed when no comment is present for the
// node.
$this->drupalLogin($this->adminUser);
$this->drupalGet('node');
$this->assertNoLink(t('@count comments', array('@count' => 0)));
$this->assertLink(t('Read more'));
// Verify the data-history-node-last-comment-timestamp attribute, which is
// used by the drupal.node-new-comments-link library to determine whether
// a "x new comments" link might be necessary or not. We do this in
// JavaScript to prevent breaking the render cache.
$this->assertIdentical(0, count($this->xpath('//*[@data-history-node-last-comment-timestamp]')), 'data-history-node-last-comment-timestamp attribute is not set.');
// Create a new comment. This helper function may be run with different
// comment settings so use $comment->save() to avoid complex setup.
/** @var \Drupal\comment\CommentInterface $comment */
$comment = entity_create('comment', array(
'cid' => NULL,
'entity_id' => $this->node->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'pid' => 0,
'uid' => $this->loggedInUser->id(),
'status' => CommentInterface::PUBLISHED,
'subject' => $this->randomMachineName(),
'hostname' => '127.0.0.1',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'comment_body' => array(LanguageInterface::LANGCODE_NOT_SPECIFIED => array($this->randomMachineName())),
));
$comment->save();
$this->drupalLogout();
// Log in with 'web user' and check comment links.
$this->drupalLogin($this->webUser);
$this->drupalGet('node');
// Verify the data-history-node-last-comment-timestamp attribute. Given its
// value, the drupal.node-new-comments-link library would determine that the
// node received a comment after the user last viewed it, and hence it would
// perform an HTTP request to render the "new comments" node link.
$this->assertIdentical(1, count($this->xpath('//*[@data-history-node-last-comment-timestamp="' . $comment->getChangedTime() . '"]')), 'data-history-node-last-comment-timestamp attribute is set to the correct value.');
$this->assertIdentical(1, count($this->xpath('//*[@data-history-node-field-name="comment"]')), 'data-history-node-field-name attribute is set to the correct value.');
// The data will be pre-seeded on this particular page in drupalSettings, to
// avoid the need for the client to make a separate request to the server.
$settings = $this->getDrupalSettings();
$this->assertEqual($settings['history'], ['lastReadTimestamps' => [1 => 0]]);
$this->assertEqual($settings['comment'], [
'newCommentsLinks' => [
'node' => [
'comment' => [
1 => [
'new_comment_count' => 1,
'first_new_comment_link' => Url::fromRoute('entity.node.canonical', ['node' => 1])->setOptions([
'fragment' => 'new',
])->toString(),
],
],
],
],
]);
// Pretend the data was not present in drupalSettings, i.e. test the
// separate request to the server.
$response = $this->renderNewCommentsNodeLinks(array($this->node->id()));
$this->assertResponse(200);
$json = Json::decode($response);
$expected = array($this->node->id() => array(
'new_comment_count' => 1,
'first_new_comment_link' => $this->node->url('canonical', array('fragment' => 'new')),
));
$this->assertIdentical($expected, $json);
// Failing to specify node IDs for the endpoint should return a 404.
$this->renderNewCommentsNodeLinks(array());
$this->assertResponse(404);
// Accessing the endpoint as the anonymous user should return a 403.
$this->drupalLogout();
$this->renderNewCommentsNodeLinks(array($this->node->id()));
$this->assertResponse(403);
$this->renderNewCommentsNodeLinks(array());
$this->assertResponse(403);
}
}

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