Move all files to 2017/

This commit is contained in:
Oliver Davies 2025-09-29 22:25:17 +01:00
parent ac7370f67f
commit 2875863330
15717 changed files with 0 additions and 0 deletions

View file

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

View file

@ -0,0 +1,65 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Entity\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 = new Breadcrumb();
$breadcrumb->addCacheContexts(['route']);
$breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
$entity = $route_match->getParameter('entity');
$breadcrumb->addLink(new Link($entity->label(), $entity->urlInfo()));
$breadcrumb->addCacheableDependency($entity);
if (($pid = $route_match->getParameter('pid')) && ($comment = $this->storage->load($pid))) {
/** @var \Drupal\comment\CommentInterface $comment */
$breadcrumb->addCacheableDependency($comment);
// Display link to parent comment.
// @todo Clean-up permalink in https://www.drupal.org/node/2198041
$breadcrumb->addLink(new Link($comment->getSubject(), $comment->urlInfo()));
}
return $breadcrumb;
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Session\AccountInterface;
/**
* 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);
}
/**
* {@inheritdoc}
*/
public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE) {
if ($operation === 'edit') {
// Only users with administer comments permission can edit the comment
// status field.
$result = AccessResult::allowedIfHasPermission($account ?: \Drupal::currentUser(), 'administer comments');
return $return_as_object ? $result : $result->isAllowed();
}
if ($operation === 'view') {
// Only users with "post comments" or "access comments" permission can
// view the field value. The formatter,
// Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter,
// takes care of showing the thread and form based on individual
// permissions, so if a user only has post comments access, only the
// form will be shown and not the comments.
$result = AccessResult::allowedIfHasPermission($account ?: \Drupal::currentUser(), 'access comments')
->orIf(AccessResult::allowedIfHasPermission($account ?: \Drupal::currentUser(), 'post comments'));
return $return_as_object ? $result : $result->isAllowed();
}
return parent::access($operation, $account, $return_as_object);
}
}

View file

@ -0,0 +1,413 @@
<?php
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base handler for comment forms.
*
* @internal
*/
class CommentForm extends ContentEntityForm {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('current_user'),
$container->get('renderer'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time'),
$container->get('entity_field.manager')
);
}
/**
* Constructs a new CommentForm.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
*/
public function __construct(EntityRepositoryInterface $entity_repository, AccountInterface $current_user, RendererInterface $renderer, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL, EntityFieldManagerInterface $entity_field_manager = NULL) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$this->currentUser = $current_user;
$this->renderer = $renderer;
$this->entityFieldManager = $entity_field_manager ?: \Drupal::service('entity_field.manager');
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
/** @var \Drupal\comment\CommentInterface $comment */
$comment = $this->entity;
$entity = $this->entityTypeManager->getStorage($comment->getCommentedEntityTypeId())->load($comment->getCommentedEntityId());
$field_name = $comment->getFieldName();
$field_definition = $this->entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle())[$comment->getFieldName()];
$config = $this->config('user.settings');
// In several places within this function, we vary $form on:
// - The current user's permissions.
// - Whether the current user is authenticated or anonymous.
// - The 'user.settings' configuration.
// - The comment field's definition.
$form['#cache']['contexts'][] = 'user.permissions';
$form['#cache']['contexts'][] = 'user.roles:authenticated';
$this->renderer->addCacheableDependency($form, $config);
$this->renderer->addCacheableDependency($form, $field_definition->getConfig($entity->bundle()));
// Use #comment-form as unique jump target, regardless of entity type.
$form['#id'] = Html::getUniqueId('comment_form');
$form['#theme'] = ['comment_form__' . $entity->getEntityTypeId() . '__' . $entity->bundle() . '__' . $field_name, 'comment_form'];
$anonymous_contact = $field_definition->getSetting('anonymous');
$is_admin = $comment->id() && $this->currentUser->hasPermission('administer comments');
if (!$this->currentUser->isAuthenticated() && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
$form['#attached']['library'][] = 'core/drupal.form';
$form['#attributes']['data-user-info-from-browser'] = TRUE;
}
// If not replying to a comment, use our dedicated page callback for new
// Comments on entities.
if (!$comment->id() && !$comment->hasParentComment()) {
$form['#action'] = $this->url('comment.reply', ['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'] = [];
// Display author information in a details element for comment moderators.
if ($is_admin) {
$form['author'] += [
'#type' => 'details',
'#title' => $this->t('Administration'),
];
}
// Prepare default values for form elements.
$author = '';
if ($is_admin) {
if (!$comment->getOwnerId()) {
$author = $comment->getAuthorName();
}
$status = $comment->getStatus();
if (empty($comment_preview)) {
$form['#title'] = $this->t('Edit comment %title', [
'%title' => $comment->getSubject(),
]);
}
}
else {
$status = ($this->currentUser->hasPermission('skip comment approval') ? CommentInterface::PUBLISHED : CommentInterface::NOT_PUBLISHED);
}
$date = '';
if ($comment->id()) {
$date = !empty($comment->date) ? $comment->date : DrupalDateTime::createFromTimestamp($comment->getCreatedTime());
}
// The uid field is only displayed when a user with the permission
// 'administer comments' is editing an existing comment from an
// authenticated user.
$owner = $comment->getOwner();
$form['author']['uid'] = [
'#type' => 'entity_autocomplete',
'#target_type' => 'user',
'#default_value' => $owner->isAnonymous() ? NULL : $owner,
// A comment can be made anonymous by leaving this field empty therefore
// there is no need to list them in the autocomplete.
'#selection_settings' => ['include_anonymous' => FALSE],
'#title' => $this->t('Authored by'),
'#description' => $this->t('Leave blank for %anonymous.', ['%anonymous' => $config->get('anonymous')]),
'#access' => $is_admin,
];
// The name field is displayed when an anonymous user is adding a comment or
// when a user with the permission 'administer comments' is editing an
// existing comment from an anonymous user.
$form['author']['name'] = [
'#type' => 'textfield',
'#title' => $is_admin ? $this->t('Name for @anonymous', ['@anonymous' => $config->get('anonymous')]) : $this->t('Your name'),
'#default_value' => $author,
'#required' => ($this->currentUser->isAnonymous() && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
'#maxlength' => 60,
'#access' => $this->currentUser->isAnonymous() || $is_admin,
'#size' => 30,
'#attributes' => [
'data-drupal-default-value' => $config->get('anonymous'),
],
];
if ($is_admin) {
// When editing a comment only display the name textfield if the uid field
// is empty.
$form['author']['name']['#states'] = [
'visible' => [
':input[name="uid"]' => ['empty' => TRUE],
],
];
}
// Add author email and homepage fields depending on the current user.
$form['author']['mail'] = [
'#type' => 'email',
'#title' => $this->t('Email'),
'#default_value' => $comment->getAuthorEmail(),
'#required' => ($this->currentUser->isAnonymous() && $anonymous_contact == 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'] = [
'#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'] = [
'#type' => 'datetime',
'#title' => $this->t('Authored on'),
'#default_value' => $date,
'#size' => 20,
'#access' => $is_admin,
];
$form['author']['status'] = [
'#type' => 'radios',
'#title' => $this->t('Status'),
'#default_value' => $status,
'#options' => [
CommentInterface::PUBLISHED => $this->t('Published'),
CommentInterface::NOT_PUBLISHED => $this->t('Not published'),
],
'#access' => $is_admin,
];
return parent::form($form, $form_state, $comment);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$element = parent::actions($form, $form_state);
/* @var \Drupal\comment\CommentInterface $comment */
$comment = $this->entity;
$entity = $comment->getCommentedEntity();
$field_definition = $this->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'] = [
'#type' => 'submit',
'#value' => $this->t('Preview'),
'#access' => $preview_mode != DRUPAL_DISABLED,
'#submit' => ['::submitForm', '::preview'],
];
return $element;
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
/** @var \Drupal\comment\CommentInterface $comment */
$comment = parent::buildEntity($form, $form_state);
if (!$form_state->isValueEmpty('date') && $form_state->getValue('date') instanceof DrupalDateTime) {
$comment->setCreatedTime($form_state->getValue('date')->getTimestamp());
}
else {
$comment->setCreatedTime(REQUEST_TIME);
}
// Empty author ID should revert to anonymous.
$author_id = $form_state->getValue('uid');
if ($comment->id() && $this->currentUser->hasPermission('administer comments')) {
// Admin can leave the author ID blank to revert to anonymous.
$author_id = $author_id ?: 0;
}
if (!is_null($author_id)) {
if ($author_id === 0 && $form['author']['name']['#access']) {
// Use the author name value when the form has access to the element and
// the author ID is anonymous.
$comment->setAuthorName($form_state->getValue('name'));
}
else {
// Ensure the author name is not set.
$comment->setAuthorName(NULL);
}
}
else {
$author_id = $this->currentUser->id();
}
$comment->setOwnerId($author_id);
// Validate the comment's subject. If not specified, extract from comment
// body.
if (trim($comment->getSubject()) == '') {
if ($comment->hasField('comment_body')) {
// 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 array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function preview(array &$form, FormStateInterface $form_state) {
$comment_preview = comment_preview($this->entity, $form_state);
$comment_preview['#title'] = $this->t('Preview comment');
$form_state->set('comment_preview', $comment_preview);
$form_state->setRebuild();
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$comment = $this->entity;
$entity = $comment->getCommentedEntity();
$field_name = $comment->getFieldName();
$uri = $entity->urlInfo();
$logger = $this->logger('comment');
if ($this->currentUser->hasPermission('post comments') && ($this->currentUser->hasPermission('administer comments') || $entity->{$field_name}->status == CommentItemInterface::OPEN)) {
$comment->save();
$form_state->setValue('cid', $comment->id());
// Add a log entry.
$logger->notice('Comment posted: %subject.', [
'%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')) {
$this->messenger()->addStatus($this->t('Your comment has been queued for review by site administrators and will be published after approval.'));
}
}
else {
$this->messenger()->addStatus($this->t('Your comment has been posted.'));
}
$query = [];
// Find the current display page for this comment.
$field_definition = $this->entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle())[$field_name];
$page = $this->entityTypeManager->getStorage('comment')->getDisplayOrdinal($comment, $field_definition->getSetting('default_mode'), $field_definition->getSetting('per_page'));
if ($page > 0) {
$query['page'] = $page;
}
// Redirect to the newly posted comment.
$uri->setOption('query', $query);
$uri->setOption('fragment', 'comment-' . $comment->id());
}
else {
$logger->warning('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', ['%subject' => $comment->getSubject()]);
$this->messenger()->addError($this->t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', ['%subject' => $comment->getSubject()]));
// Redirect the user to the entity they are commenting on.
}
$form_state->setRedirectUrl($uri);
}
}

View file

@ -0,0 +1,256 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\user\EntityOwnerInterface;
use Drupal\Core\Entity\EntityChangedInterface;
/**
* Provides an interface defining a comment entity.
*/
interface CommentInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface, EntityPublishedInterface {
/**
* Comment is awaiting approval.
*/
const NOT_PUBLISHED = 0;
/**
* Comment is published.
*/
const PUBLISHED = 1;
/**
* Anonymous posters cannot enter their contact information.
*/
const ANONYMOUS_MAYNOT_CONTACT = 0;
/**
* Anonymous posters may leave their contact information.
*/
const ANONYMOUS_MAY_CONTACT = 1;
/**
* Anonymous posters are required to leave their contact information.
*/
const ANONYMOUS_MUST_CONTACT = 2;
/**
* Determines if this comment is a reply to another comment.
*
* @return bool
* TRUE if the comment has a parent comment otherwise FALSE.
*/
public function hasParentComment();
/**
* Returns the parent comment entity if this is a reply to a comment.
*
* @return \Drupal\comment\CommentInterface|null
* A comment entity of the parent comment or NULL if there is no parent.
*/
public function getParentComment();
/**
* Returns the entity to which the comment is attached.
*
* @return \Drupal\Core\Entity\FieldableEntityInterface|null
* The entity on which the comment is attached or NULL if the comment is an
* orphan.
*/
public function getCommentedEntity();
/**
* Returns the ID of the entity to which the comment is attached.
*
* @return int
* The ID of the entity to which the comment is attached.
*/
public function getCommentedEntityId();
/**
* Returns the type of the entity to which the comment is attached.
*
* @return string
* An entity type.
*/
public function getCommentedEntityTypeId();
/**
* Sets the field ID for which this comment is attached.
*
* @param string $field_name
* The field name through which the comment was added.
*
* @return $this
* The class instance that this method is called on.
*/
public function setFieldName($field_name);
/**
* Returns the name of the field the comment is attached to.
*
* @return string
* The name of the field the comment is attached to.
*/
public function getFieldName();
/**
* Returns the subject of the comment.
*
* @return string
* The subject of the comment.
*/
public function getSubject();
/**
* Sets the subject of the comment.
*
* @param string $subject
* The subject of the comment.
*
* @return $this
* The class instance that this method is called on.
*/
public function setSubject($subject);
/**
* Returns the comment author's name.
*
* For anonymous authors, this is the value as typed in the comment form.
*
* @return string
* The name of the comment author.
*/
public function getAuthorName();
/**
* Sets the name of the author of the comment.
*
* @param string $name
* A string containing the name of the author.
*
* @return $this
* The class instance that this method is called on.
*/
public function setAuthorName($name);
/**
* Returns the comment author's email address.
*
* For anonymous authors, this is the value as typed in the comment form.
*
* @return string
* The email address of the author of the comment.
*/
public function getAuthorEmail();
/**
* Returns the comment author's home page address.
*
* For anonymous authors, this is the value as typed in the comment form.
*
* @return string
* The homepage address of the author of the comment.
*/
public function getHomepage();
/**
* Sets the comment author's home page address.
*
* For anonymous authors, this is the value as typed in the comment form.
*
* @param string $homepage
* The homepage address of the author of the comment.
*
* @return $this
* The class instance that this method is called on.
*/
public function setHomepage($homepage);
/**
* Returns the comment author's hostname.
*
* @return string
* The hostname of the author of the comment.
*/
public function getHostname();
/**
* Sets the hostname of the author of the comment.
*
* @param string $hostname
* The hostname of the author of the comment.
*
* @return $this
* The class instance that this method is called on.
*/
public function setHostname($hostname);
/**
* Returns the time that the comment was created.
*
* @return int
* The timestamp of when the comment was created.
*/
public function getCreatedTime();
/**
* Sets the creation date of the comment.
*
* @param int $created
* The timestamp of when the comment was created.
*
* @return $this
* The class instance that this method is called on.
*/
public function setCreatedTime($created);
/**
* Returns the comment's status.
*
* @return int
* One of CommentInterface::PUBLISHED or CommentInterface::NOT_PUBLISHED
*
* @deprecated in Drupal 8.3.0, will be removed before Drupal 9.0.0. Use
* \Drupal\Core\Entity\EntityPublishedInterface::isPublished() instead.
*/
public function getStatus();
/**
* 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,227 @@
<?php
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Entity\EntityFormBuilderInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
/**
* 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\RendererInterface
*/
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\RendererInterface $renderer
* The renderer service.
*/
public function __construct(EntityManagerInterface $entity_manager, EntityFormBuilderInterface $entity_form_builder, AccountInterface $current_user, CommentManagerInterface $comment_manager, ModuleHandlerInterface $module_handler, RendererInterface $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 = [
'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 = [
'#theme' => 'links__comment',
'#pre_render' => ['drupal_pre_render_links'],
'#attributes' => ['class' => ['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 = [
'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 = [];
$status = $commented_entity->get($entity->getFieldName())->status;
if ($status == CommentItemInterface::OPEN) {
if ($entity->access('delete')) {
$links['comment-delete'] = [
'title' => t('Delete'),
'url' => $entity->urlInfo('delete-form'),
];
}
if ($entity->access('update')) {
$links['comment-edit'] = [
'title' => t('Edit'),
'url' => $entity->urlInfo('edit-form'),
];
}
if ($entity->access('create')) {
$links['comment-reply'] = [
'title' => t('Reply'),
'url' => Url::fromRoute('comment.reply', [
'entity_type' => $entity->getCommentedEntityTypeId(),
'entity' => $entity->getCommentedEntityId(),
'field_name' => $entity->getFieldName(),
'pid' => $entity->id(),
]),
];
}
if (!$entity->isPublished() && $entity->access('approve')) {
$links['comment-approve'] = [
'title' => t('Approve'),
'url' => Url::fromRoute('comment.approve', ['comment' => $entity->id()]),
];
}
if (empty($links) && $this->currentUser->isAnonymous()) {
$links['comment-forbidden']['title'] = $this->commentManager->forbiddenMessage($commented_entity, $entity->getFieldName());
}
}
// Add translations link for translation-enabled comment bundles.
if ($this->moduleHandler->moduleExists('content_translation') && $this->access($entity)->isAllowed()) {
$links['comment-translations'] = [
'title' => t('Translate'),
'url' => $entity->urlInfo('drupal:content-translation-overview'),
];
}
return [
'#theme' => 'links__comment__comment',
// The "entity" property is specified to be present, so no need to check.
'#links' => $links,
'#attributes' => ['class' => ['links', 'inline']],
];
}
/**
* Wraps content_translation_translate_access.
*/
protected function access(EntityInterface $entity) {
return content_translation_translate_access($entity);
}
}

View file

@ -0,0 +1,224 @@
<?php
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\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 'log in to post new comment', 'add new comment' etc.
*/
class CommentLinkBuilder implements CommentLinkBuilderInterface {
use StringTranslationTrait;
/**
* Current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Comment manager service.
*
* @var \Drupal\comment\CommentManagerInterface
*/
protected $commentManager;
/**
* Module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The entity 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 = [];
$view_mode = $context['view_mode'];
if ($view_mode == 'search_index' || $view_mode == 'search_result' || $view_mode == 'print' || $view_mode == 'rss') {
// Do not add any links if the entity is displayed for:
// - search indexing.
// - constructing a search result excerpt.
// - print.
// - rss.
return [];
}
$fields = $this->commentManager->getFields($entity->getEntityTypeId());
foreach ($fields as $field_name => $detail) {
// Skip fields that the entity does not have.
if (!$entity->hasField($field_name)) {
continue;
}
$links = [];
$commenting_status = $entity->get($field_name)->status;
if ($commenting_status != CommentItemInterface::HIDDEN) {
// Entity has commenting status open or closed.
$field_definition = $entity->getFieldDefinition($field_name);
if ($view_mode == 'teaser') {
// Teaser view: display the number of comments that have been posted,
// or a link to add new comments if the user has permission, the
// entity is open to new comments, and there currently are none.
if ($this->currentUser->hasPermission('access comments')) {
if (!empty($entity->get($field_name)->comment_count)) {
$links['comment-comments'] = [
'title' => $this->formatPlural($entity->get($field_name)->comment_count, '1 comment', '@count comments'),
'attributes' => ['title' => $this->t('Jump to the first comment.')],
'fragment' => 'comments',
'url' => $entity->toUrl(),
];
if ($this->moduleHandler->moduleExists('history')) {
$links['comment-new-comments'] = [
'title' => '',
'url' => Url::fromRoute('<current>'),
'attributes' => [
'class' => 'hidden',
'title' => $this->t('Jump to the first new comment.'),
'data-history-node-last-comment-timestamp' => $entity->get($field_name)->last_comment_timestamp,
'data-history-node-field-name' => $field_name,
],
];
}
}
}
// Provide a link to new comment form.
if ($commenting_status == CommentItemInterface::OPEN) {
$comment_form_location = $field_definition->getSetting('form_location');
if ($this->currentUser->hasPermission('post comments')) {
$links['comment-add'] = [
'title' => $this->t('Add new comment'),
'language' => $entity->language(),
'attributes' => ['title' => $this->t('Share your thoughts and opinions.')],
'fragment' => 'comment-form',
];
if ($comment_form_location == CommentItemInterface::FORM_SEPARATE_PAGE) {
$links['comment-add']['url'] = Url::fromRoute('comment.reply', [
'entity_type' => $entity->getEntityTypeId(),
'entity' => $entity->id(),
'field_name' => $field_name,
]);
}
else {
$links['comment-add'] += ['url' => $entity->toUrl()];
}
}
elseif ($this->currentUser->isAnonymous()) {
$links['comment-forbidden'] = [
'title' => $this->commentManager->forbiddenMessage($entity, $field_name),
];
}
}
}
else {
// Entity in other view modes: add a "post comment" link if the user
// is allowed to post comments and if this entity is allowing new
// comments.
if ($commenting_status == CommentItemInterface::OPEN) {
$comment_form_location = $field_definition->getSetting('form_location');
if ($this->currentUser->hasPermission('post comments')) {
// Show the "post comment" link if the form is on another page, or
// if there are existing comments that the link will skip past.
if ($comment_form_location == CommentItemInterface::FORM_SEPARATE_PAGE || (!empty($entity->get($field_name)->comment_count) && $this->currentUser->hasPermission('access comments'))) {
$links['comment-add'] = [
'title' => $this->t('Add new comment'),
'attributes' => ['title' => $this->t('Share your thoughts and opinions.')],
'fragment' => 'comment-form',
];
if ($comment_form_location == CommentItemInterface::FORM_SEPARATE_PAGE) {
$links['comment-add']['url'] = Url::fromRoute('comment.reply', [
'entity_type' => $entity->getEntityTypeId(),
'entity' => $entity->id(),
'field_name' => $field_name,
]);
}
else {
$links['comment-add']['url'] = $entity->toUrl();
}
}
}
elseif ($this->currentUser->isAnonymous()) {
$links['comment-forbidden'] = [
'title' => $this->commentManager->forbiddenMessage($entity, $field_name),
];
}
}
}
}
if (!empty($links)) {
$entity_links['comment__' . $field_name] = [
'#theme' => 'links__entity__comment__' . $field_name,
'#links' => $links,
'#attributes' => ['class' => ['links', 'inline']],
];
if ($view_mode == 'teaser' && $this->moduleHandler->moduleExists('history') && $this->currentUser->isAuthenticated()) {
$entity_links['comment__' . $field_name]['#cache']['contexts'][] = 'user';
$entity_links['comment__' . $field_name]['#attached']['library'][] = 'comment/drupal.node-new-comments-link';
// Embed the metadata for the "X new comments" link (if any) on this
// entity.
$entity_links['comment__' . $field_name]['#attached']['drupalSettings']['history']['lastReadTimestamps'][$entity->id()] = (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, $field_name);
$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,27 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* Defines an interface for building comment links on a commented entity.
*
* Comment links include 'log in to post new comment', 'add new comment' etc.
*/
interface CommentLinkBuilderInterface {
/**
* Builds links for the given entity.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* Entity for which the links are being built.
* @param array $context
* Array of context passed from the entity view builder.
*
* @return array
* Array of entity links.
*/
public function buildCommentedEntityLinks(FieldableEntityInterface $entity, array &$context);
}

View file

@ -0,0 +1,219 @@
<?php
namespace Drupal\comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
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;
/**
* 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\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, ConfigFactoryInterface $config_factory, TranslationInterface $string_translation, UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, AccountInterface $current_user) {
$this->entityManager = $entity_manager;
$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->entityClassImplements(FieldableEntityInterface::class)) {
return [];
}
$map = $this->entityManager->getFieldMapByFieldType('comment');
return isset($map[$entity_type_id]) ? $map[$entity_type_id] : [];
}
/**
* {@inheritdoc}
*/
public function addBodyField($comment_type_id) {
if (!FieldConfig::loadByName('comment', $comment_type_id, 'comment_body')) {
// Attaches the body field by default.
$field = $this->entityManager->getStorage('field_config')->create([
'label' => 'Comment',
'bundle' => $comment_type_id,
'required' => TRUE,
'field_storage' => FieldStorageConfig::loadByName('comment', 'comment_body'),
]);
$field->save();
// Assign widget settings for the 'default' form mode.
entity_get_form_display('comment', $comment_type_id, 'default')
->setComponent('comment_body', [
'type' => 'text_textarea',
])
->save();
// Assign display settings for the 'default' view mode.
entity_get_display('comment', $comment_type_id, 'default')
->setComponent('comment_body', [
'label' => 'hidden',
'type' => 'text_default',
'weight' => 0,
])
->save();
}
}
/**
* {@inheritdoc}
*/
public function forbiddenMessage(EntityInterface $entity, $field_name) {
if (!isset($this->authenticatedCanPostComments)) {
// We only output a link if we are certain that users will get the
// permission to post comments by logging in.
$this->authenticatedCanPostComments = $this->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 = ['destination' => $this->url('comment.reply', $comment_reply_parameters, ['fragment' => 'comment-form'])];
}
else {
$destination = ['destination' => $entity->url('canonical', ['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', [
':login' => $this->urlGenerator->generateFromRoute('user.login', [], ['query' => $destination]),
':register' => $this->urlGenerator->generateFromRoute('user.register', [], ['query' => $destination]),
]);
}
else {
// Only admins can add new users, no public registration.
return $this->t('<a href=":login">Log in</a> to post comments', [
':login' => $this->urlGenerator->generateFromRoute('user.login', [], ['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->entityManager->getStorage('comment')->getQuery()
->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,80 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\EntityInterface;
/**
* Comment manager contains common functions to manage comment fields.
*/
interface CommentManagerInterface {
/**
* Comments are displayed in a flat list - expanded.
*/
const COMMENT_MODE_FLAT = 0;
/**
* Comments are displayed as a threaded list - expanded.
*/
const COMMENT_MODE_THREADED = 1;
/**
* Utility function to return an array of comment fields.
*
* @param string $entity_type_id
* The content entity type to which the comment fields are attached.
*
* @return array
* An array of comment field map definitions, keyed by field name. Each
* value is an array with two entries:
* - type: The field type.
* - bundles: The bundles in which the field appears, as an array with entity
* types as keys and the array of bundle names as values.
*
* @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,260 @@
<?php
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 ? [] : ['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 = [];
while ($entry = $stats->fetchObject()) {
$statistics_records[] = $entry;
}
return $statistics_records;
}
/**
* {@inheritdoc}
*/
public function delete(EntityInterface $entity) {
$this->database->delete('comment_entity_statistics')
->condition('entity_id', $entity->id())
->condition('entity_type', $entity->getEntityTypeId())
->execute();
}
/**
* {@inheritdoc}
*/
public function create(FieldableEntityInterface $entity, $fields) {
$query = $this->database->insert('comment_entity_statistics')
->fields([
'entity_id',
'entity_type',
'field_name',
'cid',
'last_comment_timestamp',
'last_comment_name',
'last_comment_uid',
'comment_count',
]);
foreach ($fields as $field_name => $detail) {
// Skip fields that entity does not have.
if (!$entity->hasField($field_name)) {
continue;
}
// Get the user ID from the entity if it's set, or default to the
// currently logged in user.
$last_comment_uid = 0;
if ($entity instanceof EntityOwnerInterface) {
$last_comment_uid = $entity->getOwnerId();
}
if (!isset($last_comment_uid)) {
// Default to current user when entity does not implement
// EntityOwnerInterface or author is not set.
$last_comment_uid = $this->currentUser->id();
}
// Default to REQUEST_TIME when entity does not have a changed property.
$last_comment_timestamp = 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([
'entity_id' => $entity->id(),
'entity_type' => $entity->getEntityTypeId(),
'field_name' => $field_name,
'cid' => 0,
'last_comment_timestamp' => $last_comment_timestamp,
'last_comment_name' => NULL,
'last_comment_uid' => $last_comment_uid,
'comment_count' => 0,
]);
}
$query->execute();
}
/**
* {@inheritdoc}
*/
public function getMaximumCount($entity_type) {
return $this->database->query('SELECT MAX(comment_count) FROM {comment_entity_statistics} WHERE entity_type = :entity_type', [':entity_type' => $entity_type])->fetchField();
}
/**
* {@inheritdoc}
*/
public function getRankingInfo() {
return [
'comments' => [
'title' => t('Number of comments'),
'join' => [
'type' => 'LEFT',
'table' => 'comment_entity_statistics',
'alias' => 'ces',
// Default to comment field as this is the most common use case for
// nodes.
'on' => "ces.entity_id = i.sid AND ces.entity_type = 'node' AND ces.field_name = 'comment'",
],
// Inverse law that maps the highest view count on the site to 1 and 0
// to 0. Note that the ROUND here is necessary for PostgreSQL and SQLite
// in order to ensure that the :comment_scale argument is treated as
// a numeric type, because the PostgreSQL PDO driver sometimes puts
// values in as strings instead of numbers in complex expressions like
// this.
'score' => '2.0 - 2.0 / (1.0 + ces.comment_count * (ROUND(:comment_scale, 4)))',
'arguments' => [':comment_scale' => \Drupal::state()->get('comment.node_comment_statistics_scale') ?: 0],
],
];
}
/**
* {@inheritdoc}
*/
public function update(CommentInterface $comment) {
// Allow bulk updates and inserts to temporarily disable the maintenance of
// the {comment_entity_statistics} table.
if (!$this->state->get('comment.maintain_entity_statistics')) {
return;
}
$query = $this->database->select('comment_field_data', 'c');
$query->addExpression('COUNT(cid)');
$count = $query->condition('c.entity_id', $comment->getCommentedEntityId())
->condition('c.entity_type', $comment->getCommentedEntityTypeId())
->condition('c.field_name', $comment->getFieldName())
->condition('c.status', CommentInterface::PUBLISHED)
->condition('default_langcode', 1)
->execute()
->fetchField();
if ($count > 0) {
// Comments exist.
$last_reply = $this->database->select('comment_field_data', 'c')
->fields('c', ['cid', 'name', 'changed', 'uid'])
->condition('c.entity_id', $comment->getCommentedEntityId())
->condition('c.entity_type', $comment->getCommentedEntityTypeId())
->condition('c.field_name', $comment->getFieldName())
->condition('c.status', CommentInterface::PUBLISHED)
->condition('default_langcode', 1)
->orderBy('c.created', 'DESC')
->range(0, 1)
->execute()
->fetchObject();
// Use merge here because entity could be created before comment field.
$this->database->merge('comment_entity_statistics')
->fields([
'cid' => $last_reply->cid,
'comment_count' => $count,
'last_comment_timestamp' => $last_reply->changed,
'last_comment_name' => $last_reply->uid ? '' : $last_reply->name,
'last_comment_uid' => $last_reply->uid,
])
->keys([
'entity_id' => $comment->getCommentedEntityId(),
'entity_type' => $comment->getCommentedEntityTypeId(),
'field_name' => $comment->getFieldName(),
])
->execute();
}
else {
// Comments do not exist.
$entity = $comment->getCommentedEntity();
// Get the user ID from the entity if it's set, or default to the
// currently logged in user.
if ($entity instanceof EntityOwnerInterface) {
$last_comment_uid = $entity->getOwnerId();
}
if (!isset($last_comment_uid)) {
// Default to current user when entity does not implement
// EntityOwnerInterface or author is not set.
$last_comment_uid = $this->currentUser->id();
}
$this->database->update('comment_entity_statistics')
->fields([
'cid' => 0,
'comment_count' => 0,
// Use the changed date of the entity if it's set, or default to
// REQUEST_TIME.
'last_comment_timestamp' => ($entity instanceof EntityChangedInterface) ? $entity->getChangedTimeAcrossTranslations() : 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([$comment->getCommentedEntityId()]);
}
}

View file

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

View file

@ -0,0 +1,342 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
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 storage handler class for comments.
*
* This extends the Drupal\Core\Entity\Sql\SqlContentEntityStorage class,
* adding required special handling for comment entities.
*/
class CommentStorage extends SqlContentEntityStorage implements CommentStorageInterface {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a CommentStorage object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_info
* An array of entity info for the entity type.
* @param \Drupal\Core\Database\Connection $database
* The database connection to be used.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* Cache backend instance to use.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
* The memory cache.
*/
public function __construct(EntityTypeInterface $entity_info, Connection $database, EntityManagerInterface $entity_manager, AccountInterface $current_user, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache) {
parent::__construct($entity_info, $database, $entity_manager, $cache, $language_manager, $memory_cache);
$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'),
$container->get('entity.memory_cache')
);
}
/**
* {@inheritdoc}
*/
public function getMaxThread(CommentInterface $comment) {
$query = $this->database->select($this->getDataTable(), 'c')
->condition('entity_id', $comment->getCommentedEntityId())
->condition('field_name', $comment->getFieldName())
->condition('entity_type', $comment->getCommentedEntityTypeId())
->condition('default_langcode', 1);
$query->addExpression('MAX(thread)', 'thread');
return $query->execute()
->fetchField();
}
/**
* {@inheritdoc}
*/
public function getMaxThreadPerThread(CommentInterface $comment) {
$query = $this->database->select($this->getDataTable(), 'c')
->condition('entity_id', $comment->getCommentedEntityId())
->condition('field_name', $comment->getFieldName())
->condition('entity_type', $comment->getCommentedEntityTypeId())
->condition('thread', $comment->getParentComment()->getThread() . '.%', 'LIKE')
->condition('default_langcode', 1);
$query->addExpression('MAX(thread)', 'thread');
return $query->execute()
->fetchField();
}
/**
* {@inheritdoc}
*/
public function getDisplayOrdinal(CommentInterface $comment, $comment_mode, $divisor = 1) {
// Count how many comments (c1) are before $comment (c2) in display order.
// This is the 0-based display ordinal.
$data_table = $this->getDataTable();
$query = $this->database->select($data_table, 'c1');
$query->innerJoin($data_table, 'c2', 'c2.entity_id = c1.entity_id AND c2.entity_type = c1.entity_type AND c2.field_name = c1.field_name');
$query->addExpression('COUNT(*)', 'count');
$query->condition('c2.cid', $comment->id());
if (!$this->currentUser->hasPermission('administer comments')) {
$query->condition('c1.status', CommentInterface::PUBLISHED);
}
if ($comment_mode == CommentManagerInterface::COMMENT_MODE_FLAT) {
// For rendering flat comments, cid is used for ordering comments due to
// unpredictable behavior with timestamp, so we make the same assumption
// here.
$query->condition('c1.cid', $comment->id(), '<');
}
else {
// For threaded comments, the c.thread column is used for ordering. We can
// use the sorting code for comparison, but must remove the trailing
// slash.
$query->where('SUBSTRING(c1.thread, 1, (LENGTH(c1.thread) - 1)) < SUBSTRING(c2.thread, 1, (LENGTH(c2.thread) - 1))');
}
$query->condition('c1.default_langcode', 1);
$query->condition('c2.default_langcode', 1);
$ordinal = $query->execute()->fetchField();
return ($divisor > 1) ? floor($ordinal / $divisor) : $ordinal;
}
/**
* {@inheritdoc}
*/
public function getNewCommentPageNumber($total_comments, $new_comments, FieldableEntityInterface $entity, $field_name) {
$field = $entity->getFieldDefinition($field_name);
$comments_per_page = $field->getSetting('per_page');
$data_table = $this->getDataTable();
if ($total_comments <= $comments_per_page) {
// Only one page of comments.
$count = 0;
}
elseif ($field->getSetting('default_mode') == CommentManagerInterface::COMMENT_MODE_FLAT) {
// Flat comments.
$count = $total_comments - $new_comments;
}
else {
// Threaded comments.
// 1. Find all the threads with a new comment.
$unread_threads_query = $this->database->select($data_table, 'comment')
->fields('comment', ['thread'])
->condition('entity_id', $entity->id())
->condition('entity_type', $entity->getEntityTypeId())
->condition('field_name', $field_name)
->condition('status', CommentInterface::PUBLISHED)
->condition('default_langcode', 1)
->orderBy('created', 'DESC')
->orderBy('cid', 'DESC')
->range(0, $new_comments);
// 2. Find the first thread.
$first_thread_query = $this->database->select($unread_threads_query, 'thread');
$first_thread_query->addExpression('SUBSTRING(thread, 1, (LENGTH(thread) - 1))', 'torder');
$first_thread = $first_thread_query
->fields('thread', ['thread'])
->orderBy('torder')
->range(0, 1)
->execute()
->fetchField();
// Remove the final '/'.
$first_thread = substr($first_thread, 0, -1);
// Find the number of the first comment of the first unread thread.
$count = $this->database->query('SELECT COUNT(*) FROM {' . $data_table . '} WHERE entity_id = :entity_id
AND entity_type = :entity_type
AND field_name = :field_name
AND status = :status
AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread
AND default_langcode = 1', [
':status' => CommentInterface::PUBLISHED,
':entity_id' => $entity->id(),
':field_name' => $field_name,
':entity_type' => $entity->getEntityTypeId(),
':thread' => $first_thread,
])->fetchField();
}
return $comments_per_page > 0 ? (int) ($count / $comments_per_page) : 0;
}
/**
* {@inheritdoc}
*/
public function getChildCids(array $comments) {
return $this->database->select($this->getDataTable(), 'c')
->fields('c', ['cid'])
->condition('pid', array_keys($comments), 'IN')
->condition('default_langcode', 1)
->execute()
->fetchCol();
}
/**
* {@inheritdoc}
*
* To display threaded comments in the correct order we keep a 'thread' field
* and order by that value. This field keeps this data in
* a way which is easy to update and convenient to use.
*
* A "thread" value starts at "1". If we add a child (A) to this comment,
* we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
* brother of (A) will get "1.2". Next brother of the parent of (A) will get
* "2" and so on.
*
* First of all note that the thread field stores the depth of the comment:
* depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
*
* Now to get the ordering right, consider this example:
*
* 1
* 1.1
* 1.1.1
* 1.2
* 2
*
* If we "ORDER BY thread ASC" we get the above result, and this is the
* natural order sorted by time. However, if we "ORDER BY thread DESC"
* we get:
*
* 2
* 1.2
* 1.1.1
* 1.1
* 1
*
* Clearly, this is not a natural way to see a thread, and users will get
* confused. The natural order to show a thread by time desc would be:
*
* 2
* 1
* 1.2
* 1.1
* 1.1.1
*
* which is what we already did before the standard pager patch. To achieve
* this we simply add a "/" at the end of each "thread" value. This way, the
* thread fields will look like this:
*
* 1/
* 1.1/
* 1.1.1/
* 1.2/
* 2/
*
* we add "/" since this char is, in ASCII, higher than every number, so if
* now we "ORDER BY thread DESC" we get the correct order. However this would
* spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
* to consider the trailing "/" so we use a substring only.
*/
public function loadThread(EntityInterface $entity, $field_name, $mode, $comments_per_page = 0, $pager_id = 0) {
$data_table = $this->getDataTable();
$query = $this->database->select($data_table, 'c');
$query->addField('c', 'cid');
$query
->condition('c.entity_id', $entity->id())
->condition('c.entity_type', $entity->getEntityTypeId())
->condition('c.field_name', $field_name)
->condition('c.default_langcode', 1)
->addTag('entity_access')
->addTag('comment_filter')
->addMetaData('base_table', 'comment')
->addMetaData('entity', $entity)
->addMetaData('field_name', $field_name);
if ($comments_per_page) {
$query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->limit($comments_per_page);
if ($pager_id) {
$query->element($pager_id);
}
$count_query = $this->database->select($data_table, 'c');
$count_query->addExpression('COUNT(*)');
$count_query
->condition('c.entity_id', $entity->id())
->condition('c.entity_type', $entity->getEntityTypeId())
->condition('c.field_name', $field_name)
->condition('c.default_langcode', 1)
->addTag('entity_access')
->addTag('comment_filter')
->addMetaData('base_table', 'comment')
->addMetaData('entity', $entity)
->addMetaData('field_name', $field_name);
$query->setCountQuery($count_query);
}
if (!$this->currentUser->hasPermission('administer comments')) {
$query->condition('c.status', CommentInterface::PUBLISHED);
if ($comments_per_page) {
$count_query->condition('c.status', CommentInterface::PUBLISHED);
}
}
if ($mode == CommentManagerInterface::COMMENT_MODE_FLAT) {
$query->orderBy('c.cid', 'ASC');
}
else {
// See comment above. Analysis reveals that this doesn't cost too
// much. It scales much much better than having the whole comment
// structure.
$query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
$query->orderBy('torder', 'ASC');
}
$cids = $query->execute()->fetchCol();
$comments = [];
if ($cids) {
$comments = $this->loadMultiple($cids);
}
return $comments;
}
/**
* {@inheritdoc}
*/
public function getUnapprovedCount() {
return $this->database->select($this->getDataTable(), 'c')
->condition('status', CommentInterface::NOT_PUBLISHED, '=')
->condition('default_langcode', 1)
->countQuery()
->execute()
->fetchField();
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* Defines an interface for comment entity storage classes.
*/
interface CommentStorageInterface extends ContentEntityStorageInterface {
/**
* Gets the maximum encoded thread value for the top level comments.
*
* @param \Drupal\comment\CommentInterface $comment
* A comment entity.
*
* @return string
* 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);
/**
* 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,75 @@
<?php
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);
if ($data_table = $this->storage->getDataTable()) {
$schema[$data_table]['indexes'] += [
'comment__status_pid' => ['pid', 'status'],
'comment__num_new' => [
'entity_id',
'entity_type',
'comment_type',
'status',
'created',
'cid',
'thread',
],
'comment__entity_langcode' => [
'entity_id',
'entity_type',
'comment_type',
'default_langcode',
],
];
}
return $schema;
}
/**
* {@inheritdoc}
*/
protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
$schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
$field_name = $storage_definition->getName();
if ($table_name == 'comment_field_data') {
// Remove unneeded indexes.
unset($schema['indexes']['comment_field__pid__target_id']);
unset($schema['indexes']['comment_field__entity_id__target_id']);
switch ($field_name) {
case 'thread':
// Improves the performance of the comment__num_new index defined
// in getEntitySchema().
$schema['fields'][$field_name]['not null'] = TRUE;
break;
case 'created':
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
break;
case 'uid':
$this->addSharedTableFieldForeignKey($storage_definition, $schema, 'users', 'uid');
}
}
return $schema;
}
}

View file

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

View file

@ -0,0 +1,175 @@
<?php
namespace Drupal\comment;
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 handler for comment type edit forms.
*
* @internal
*/
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'] = [
'#type' => 'textfield',
'#title' => t('Label'),
'#maxlength' => 255,
'#default_value' => $comment_type->label(),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $comment_type->id(),
'#machine_name' => [
'exists' => '\Drupal\comment\Entity\CommentType::load',
],
'#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
'#disabled' => !$comment_type->isNew(),
];
$form['description'] = [
'#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 = [];
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'] = [
'#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'] = [
'#type' => 'item',
'#markup' => $this->entityManager->getDefinition($comment_type->getTargetEntityTypeId())->getLabel(),
'#title' => t('Target entity type'),
];
}
if ($this->moduleHandler->moduleExists('content_translation')) {
$form['language'] = [
'#type' => 'details',
'#title' => t('Language settings'),
'#group' => 'additional_settings',
];
$language_configuration = ContentLanguageSettings::loadByEntityTypeBundle('comment', $comment_type->id());
$form['language']['language_configuration'] = [
'#type' => 'language_configuration',
'#entity_information' => [
'entity_type' => 'comment',
'bundle' => $comment_type->id(),
],
'#default_value' => $language_configuration,
];
$form['#submit'][] = 'language_configuration_element_submit';
}
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => 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) {
$this->messenger()->addStatus(t('Comment type %label has been updated.', ['%label' => $comment_type->label()]));
$this->logger->notice('Comment type %label has been updated.', ['%label' => $comment_type->label(), 'link' => $edit_link]);
}
else {
$this->commentManager->addBodyField($comment_type->id());
$this->messenger()->addStatus(t('Comment type %label has been added.', ['%label' => $comment_type->label()]));
$this->logger->notice('Comment type %label has been added.', ['%label' => $comment_type->label(), 'link' => $edit_link]);
}
$form_state->setRedirectUrl($comment_type->urlInfo('collection'));
}
}

View file

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

View file

@ -0,0 +1,46 @@
<?php
namespace Drupal\comment;
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'] = $entity->label();
$row['description']['data'] = ['#markup' => $entity->getDescription()];
return $row + parent::buildRow($entity);
}
}

View file

@ -0,0 +1,187 @@
<?php
namespace Drupal\comment;
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\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* View builder handler 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) {
$build = parent::getBuildDefaults($entity, $view_mode);
/** @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 cacheability metadata, so it can bubble up.
if ($build['#comment_threaded']) {
unset($build['#cache']['keys']);
}
return $build;
}
/**
* {@inheritdoc}
*
* In addition to modifying the content key on entities, this implementation
* will also set the comment entity key which all comments carry.
*
* @throws \InvalidArgumentException
* Thrown when a comment is attached to an entity that no longer exists.
*/
public function buildComponents(array &$build, array $entities, array $displays, $view_mode) {
/** @var \Drupal\comment\CommentInterface[] $entities */
if (empty($entities)) {
return;
}
// Pre-load associated users into cache to leverage multiple loading.
$uids = [];
foreach ($entities as $entity) {
$uids[] = $entity->getOwnerId();
}
$this->entityManager->getStorage('user')->loadMultiple(array_unique($uids));
parent::buildComponents($build, $entities, $displays, $view_mode);
// A counter to track the indentation level.
$current_indent = 0;
$attach_history = $this->moduleHandler->moduleExists('history') && $this->currentUser->isAuthenticated();
foreach ($entities as $id => $entity) {
if ($build[$id]['#comment_threaded']) {
$comment_indent = count(explode('.', $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'] = [
'#lazy_builder' => [
'comment.lazy_builders:renderLinks',
[
$entity->id(),
$view_mode,
$entity->language()->getId(),
!empty($entity->in_preview),
],
],
'#create_placeholder' => TRUE,
];
}
if (!isset($build[$id]['#attached'])) {
$build[$id]['#attached'] = [];
}
$build[$id]['#attached']['library'][] = 'comment/drupal.comment-by-viewer';
if ($attach_history && $commented_entity->getEntityTypeId() === 'node') {
$build[$id]['#attached']['library'][] = 'comment/drupal.comment-new-indicator';
// Embed the metadata for the comment "new" indicators on this node.
$build[$id]['history'] = [
'#lazy_builder' => ['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) {
parent::alterBuild($build, $comment, $display, $view_mode);
if (empty($comment->in_preview)) {
$prefix = '';
// Add indentation div or close open divs as needed.
if ($build['#comment_threaded']) {
$prefix .= $build['#comment_indent'] <= 0 ? str_repeat('</div>', abs($build['#comment_indent'])) : "\n" . '<div class="indented">';
}
// 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,397 @@
<?php
namespace Drupal\comment;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\views\EntityViewsData;
/**
* Provides views data for the comment entity type.
*/
class CommentViewsData extends EntityViewsData {
/**
* {@inheritdoc}
*/
public function getViewsData() {
$data = parent::getViewsData();
$data['comment_field_data']['table']['base']['help'] = $this->t('Comments are responses to content.');
$data['comment_field_data']['table']['base']['access query tag'] = 'comment_access';
$data['comment_field_data']['table']['wizard_id'] = 'comment';
$data['comment_field_data']['subject']['title'] = $this->t('Title');
$data['comment_field_data']['subject']['help'] = $this->t('The title of the comment.');
$data['comment_field_data']['subject']['field']['default_formatter'] = 'comment_permalink';
$data['comment_field_data']['name']['title'] = $this->t('Author');
$data['comment_field_data']['name']['help'] = $this->t("The name of the comment's author. Can be rendered as a link to the author's homepage.");
$data['comment_field_data']['name']['field']['default_formatter'] = 'comment_username';
$data['comment_field_data']['homepage']['title'] = $this->t("Author's website");
$data['comment_field_data']['homepage']['help'] = $this->t("The website address of the comment's author. Can be rendered as a link. Will be empty if the author is a registered user.");
$data['comment_field_data']['mail']['help'] = $this->t('Email of user that posted the comment. Will be empty if the author is a registered user.');
$data['comment_field_data']['created']['title'] = $this->t('Post date');
$data['comment_field_data']['created']['help'] = $this->t('Date and time of when the comment was created.');
$data['comment_field_data']['created_fulldata'] = [
'title' => $this->t('Created date'),
'help' => $this->t('Date in the form of CCYYMMDD.'),
'argument' => [
'field' => 'created',
'id' => 'date_fulldate',
],
];
$data['comment_field_data']['created_year_month'] = [
'title' => $this->t('Created year + month'),
'help' => $this->t('Date in the form of YYYYMM.'),
'argument' => [
'field' => 'created',
'id' => 'date_year_month',
],
];
$data['comment_field_data']['created_year'] = [
'title' => $this->t('Created year'),
'help' => $this->t('Date in the form of YYYY.'),
'argument' => [
'field' => 'created',
'id' => 'date_year',
],
];
$data['comment_field_data']['created_month'] = [
'title' => $this->t('Created month'),
'help' => $this->t('Date in the form of MM (01 - 12).'),
'argument' => [
'field' => 'created',
'id' => 'date_month',
],
];
$data['comment_field_data']['created_day'] = [
'title' => $this->t('Created day'),
'help' => $this->t('Date in the form of DD (01 - 31).'),
'argument' => [
'field' => 'created',
'id' => 'date_day',
],
];
$data['comment_field_data']['created_week'] = [
'title' => $this->t('Created week'),
'help' => $this->t('Date in the form of WW (01 - 53).'),
'argument' => [
'field' => 'created',
'id' => 'date_week',
],
];
$data['comment_field_data']['changed']['title'] = $this->t('Updated date');
$data['comment_field_data']['changed']['help'] = $this->t('Date and time of when the comment was last updated.');
$data['comment_field_data']['changed_fulldata'] = [
'title' => $this->t('Changed date'),
'help' => $this->t('Date in the form of CCYYMMDD.'),
'argument' => [
'field' => 'changed',
'id' => 'date_fulldate',
],
];
$data['comment_field_data']['changed_year_month'] = [
'title' => $this->t('Changed year + month'),
'help' => $this->t('Date in the form of YYYYMM.'),
'argument' => [
'field' => 'changed',
'id' => 'date_year_month',
],
];
$data['comment_field_data']['changed_year'] = [
'title' => $this->t('Changed year'),
'help' => $this->t('Date in the form of YYYY.'),
'argument' => [
'field' => 'changed',
'id' => 'date_year',
],
];
$data['comment_field_data']['changed_month'] = [
'title' => $this->t('Changed month'),
'help' => $this->t('Date in the form of MM (01 - 12).'),
'argument' => [
'field' => 'changed',
'id' => 'date_month',
],
];
$data['comment_field_data']['changed_day'] = [
'title' => $this->t('Changed day'),
'help' => $this->t('Date in the form of DD (01 - 31).'),
'argument' => [
'field' => 'changed',
'id' => 'date_day',
],
];
$data['comment_field_data']['changed_week'] = [
'title' => $this->t('Changed week'),
'help' => $this->t('Date in the form of WW (01 - 53).'),
'argument' => [
'field' => 'changed',
'id' => 'date_week',
],
];
$data['comment_field_data']['status']['title'] = $this->t('Approved status');
$data['comment_field_data']['status']['help'] = $this->t('Whether the comment is approved (or still in the moderation queue).');
$data['comment_field_data']['status']['filter']['label'] = $this->t('Approved comment status');
$data['comment_field_data']['status']['filter']['type'] = 'yes-no';
$data['comment']['approve_comment'] = [
'field' => [
'title' => $this->t('Link to approve comment'),
'help' => $this->t('Provide a simple link to approve the comment.'),
'id' => 'comment_link_approve',
],
];
$data['comment']['replyto_comment'] = [
'field' => [
'title' => $this->t('Link to reply-to comment'),
'help' => $this->t('Provide a simple link to reply to the comment.'),
'id' => 'comment_link_reply',
],
];
$data['comment_field_data']['entity_id']['field']['id'] = 'commented_entity';
unset($data['comment_field_data']['entity_id']['relationship']);
$data['comment']['comment_bulk_form'] = [
'title' => $this->t('Comment operations bulk form'),
'help' => $this->t('Add a form element that lets you run operations on multiple comments.'),
'field' => [
'id' => 'comment_bulk_form',
],
];
$data['comment_field_data']['thread']['field'] = [
'title' => $this->t('Depth'),
'help' => $this->t('Display the depth of the comment if it is threaded.'),
'id' => 'comment_depth',
];
$data['comment_field_data']['thread']['sort'] = [
'title' => $this->t('Thread'),
'help' => $this->t('Sort by the threaded order. This will keep child comments together with their parents.'),
'id' => 'comment_thread',
];
unset($data['comment_field_data']['thread']['filter']);
unset($data['comment_field_data']['thread']['argument']);
$entities_types = \Drupal::entityManager()->getDefinitions();
// Provide a relationship for each entity type except comment.
foreach ($entities_types as $type => $entity_type) {
if ($type == 'comment' || !$entity_type->entityClassImplements(ContentEntityInterface::class) || !$entity_type->getBaseTable()) {
continue;
}
if ($fields = \Drupal::service('comment.manager')->getFields($type)) {
$data['comment_field_data'][$type] = [
'relationship' => [
'title' => $entity_type->getLabel(),
'help' => $this->t('The @entity_type to which the comment is a reply to.', ['@entity_type' => $entity_type->getLabel()]),
'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
'base field' => $entity_type->getKey('id'),
'relationship field' => 'entity_id',
'id' => 'standard',
'label' => $entity_type->getLabel(),
'extra' => [
[
'field' => 'entity_type',
'value' => $type,
'table' => 'comment_field_data',
],
],
],
];
}
}
$data['comment_field_data']['uid']['title'] = $this->t('Author uid');
$data['comment_field_data']['uid']['help'] = $this->t('If you need more fields than the uid add the comment: author relationship');
$data['comment_field_data']['uid']['relationship']['title'] = $this->t('Author');
$data['comment_field_data']['uid']['relationship']['help'] = $this->t("The User ID of the comment's author.");
$data['comment_field_data']['uid']['relationship']['label'] = $this->t('author');
$data['comment_field_data']['pid']['title'] = $this->t('Parent CID');
$data['comment_field_data']['pid']['relationship']['title'] = $this->t('Parent comment');
$data['comment_field_data']['pid']['relationship']['help'] = $this->t('The parent comment');
$data['comment_field_data']['pid']['relationship']['label'] = $this->t('parent');
// Define the base group of this table. Fields that don't have a group defined
// will go into this field by default.
$data['comment_entity_statistics']['table']['group'] = $this->t('Comment Statistics');
// Provide a relationship for each entity type except comment.
foreach ($entities_types as $type => $entity_type) {
if ($type == 'comment' || !$entity_type->entityClassImplements(ContentEntityInterface::class) || !$entity_type->getBaseTable()) {
continue;
}
// This relationship does not use the 'field id' column, if the entity has
// multiple comment-fields, then this might introduce duplicates, in which
// case the site-builder should enable aggregation and SUM the comment_count
// field. We cannot create a relationship from the base table to
// {comment_entity_statistics} for each field as multiple joins between
// the same two tables is not supported.
if (\Drupal::service('comment.manager')->getFields($type)) {
$data['comment_entity_statistics']['table']['join'][$entity_type->getDataTable() ?: $entity_type->getBaseTable()] = [
'type' => 'INNER',
'left_field' => $entity_type->getKey('id'),
'field' => 'entity_id',
'extra' => [
[
'field' => 'entity_type',
'value' => $type,
],
],
];
}
}
$data['comment_entity_statistics']['last_comment_timestamp'] = [
'title' => $this->t('Last comment time'),
'help' => $this->t('Date and time of when the last comment was posted.'),
'field' => [
'id' => 'comment_last_timestamp',
],
'sort' => [
'id' => 'date',
],
'filter' => [
'id' => 'date',
],
];
$data['comment_entity_statistics']['last_comment_name'] = [
'title' => $this->t("Last comment author"),
'help' => $this->t('The name of the author of the last posted comment.'),
'field' => [
'id' => 'comment_ces_last_comment_name',
'no group by' => TRUE,
],
'sort' => [
'id' => 'comment_ces_last_comment_name',
'no group by' => TRUE,
],
];
$data['comment_entity_statistics']['comment_count'] = [
'title' => $this->t('Comment count'),
'help' => $this->t('The number of comments an entity has.'),
'field' => [
'id' => 'numeric',
],
'filter' => [
'id' => 'numeric',
],
'sort' => [
'id' => 'standard',
],
'argument' => [
'id' => 'standard',
],
];
$data['comment_entity_statistics']['last_updated'] = [
'title' => $this->t('Updated/commented date'),
'help' => $this->t('The most recent of last comment posted or entity updated time.'),
'field' => [
'id' => 'comment_ces_last_updated',
'no group by' => TRUE,
],
'sort' => [
'id' => 'comment_ces_last_updated',
'no group by' => TRUE,
],
'filter' => [
'id' => 'comment_ces_last_updated',
],
];
$data['comment_entity_statistics']['cid'] = [
'title' => $this->t('Last comment CID'),
'help' => $this->t('Display the last comment of an entity'),
'relationship' => [
'title' => $this->t('Last comment'),
'help' => $this->t('The last comment of an entity.'),
'group' => $this->t('Comment'),
'base' => 'comment',
'base field' => 'cid',
'id' => 'standard',
'label' => $this->t('Last Comment'),
],
];
$data['comment_entity_statistics']['last_comment_uid'] = [
'title' => $this->t('Last comment uid'),
'help' => $this->t('The User ID of the author of the last comment of an entity.'),
'relationship' => [
'title' => $this->t('Last comment author'),
'base' => 'users',
'base field' => 'uid',
'id' => 'standard',
'label' => $this->t('Last comment author'),
],
'filter' => [
'id' => 'numeric',
],
'argument' => [
'id' => 'numeric',
],
'field' => [
'id' => 'numeric',
],
];
$data['comment_entity_statistics']['entity_type'] = [
'title' => $this->t('Entity type'),
'help' => $this->t('The entity type to which the comment is a reply to.'),
'field' => [
'id' => 'standard',
],
'filter' => [
'id' => 'string',
],
'argument' => [
'id' => 'string',
],
'sort' => [
'id' => 'standard',
],
];
$data['comment_entity_statistics']['field_name'] = [
'title' => $this->t('Comment field name'),
'help' => $this->t('The field name from which the comment originated.'),
'field' => [
'id' => 'standard',
],
'filter' => [
'id' => 'string',
],
'argument' => [
'id' => 'string',
],
'sort' => [
'id' => 'standard',
],
];
return $data;
}
}

View file

@ -0,0 +1,346 @@
<?php
namespace Drupal\comment\Controller;
use Drupal\comment\CommentInterface;
use Drupal\comment\CommentManagerInterface;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\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
*/
public function commentApprove(CommentInterface $comment) {
$comment->setPublished();
$comment->save();
$this->messenger()->addStatus($this->t('Comment approved.'));
$permalink_uri = $comment->permalink();
$permalink_uri->setAbsolute();
return new RedirectResponse($permalink_uri->toString());
}
/**
* Redirects comment links to the correct page depending on comment settings.
*
* Since comments are paged there is no way to guarantee which page a comment
* appears on. Comment paging and threading settings may be changed at any
* time. With threaded comments, an individual comment may move between pages
* as comments can be added either before or after it in the overall
* discussion. Therefore we use a central routing function for comment links,
* which calculates the page number based on current comment settings and
* returns the full comment view with the pager set dynamically.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request of the page.
* @param \Drupal\comment\CommentInterface $comment
* A comment entity.
*
* @return \Symfony\Component\HttpFoundation\Response
* The comment listing set to the page on which the comment appears.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function commentPermalink(Request $request, CommentInterface $comment) {
if ($entity = $comment->getCommentedEntity()) {
// Check access permissions for the entity.
if (!$entity->access('view')) {
throw new AccessDeniedHttpException();
}
$field_definition = $this->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.
$subrequest_url = $entity->urlInfo()->setOption('query', ['page' => $page])->toString(TRUE);
$redirect_request = Request::create($subrequest_url->getGeneratedUrl(), 'GET', $request->query->all(), $request->cookies->all(), [], $request->server->all());
// Carry over the session to the subrequest.
if ($session = $request->getSession()) {
$redirect_request->setSession($session);
}
$request->query->set('page', $page);
$response = $this->httpKernel->handle($redirect_request, HttpKernelInterface::SUB_REQUEST);
if ($response instanceof CacheableResponseInterface) {
// @todo Once path aliases have cache tags (see
// https://www.drupal.org/node/2480077), add test coverage that
// the cache tag for a commented entity's path alias is added to the
// comment's permalink response, because there can be blocks or
// other content whose renderings depend on the subrequest's URL.
$response->addCacheableDependency($subrequest_url);
}
return $response;
}
throw new NotFoundHttpException();
}
/**
* The _title_callback for the page that renders the comment permalink.
*
* @param \Drupal\comment\CommentInterface $comment
* The current comment.
*
* @return string
* The translated comment subject.
*/
public function commentPermalinkTitle(CommentInterface $comment) {
return $this->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.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Redirects user to new url.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function redirectNode(EntityInterface $node) {
$fields = $this->commentManager->getFields('node');
// Legacy nodes only had a single comment field, so use the first comment
// field on the entity.
if (!empty($fields) && ($field_names = array_keys($fields)) && ($field_name = reset($field_names))) {
return $this->redirect('comment.reply', [
'entity_type' => 'node',
'entity' => $node->id(),
'field_name' => $field_name,
]);
}
throw new NotFoundHttpException();
}
/**
* Form constructor for the comment reply form.
*
* There are several cases that have to be handled, including:
* - replies to comments
* - replies to entities
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity this comment belongs to.
* @param string $field_name
* The field_name to which the comment belongs.
* @param int $pid
* (optional) Some comments are replies to other comments. In those cases,
* $pid is the parent comment's comment ID. Defaults to NULL.
*
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
* An associative array containing:
* - An array for rendering the entity or parent comment.
* - comment_entity: If the comment is a reply to the entity.
* - comment_parent: If the comment is a reply to another comment.
* - comment_form: The comment form as a renderable array.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function getReplyForm(Request $request, EntityInterface $entity, $field_name, $pid = NULL) {
$account = $this->currentUser();
$build = [];
// The user is not just previewing a comment.
if ($request->request->get('op') != $this->t('Preview')) {
// $pid indicates that this is a reply to a comment.
if ($pid) {
// Load the parent comment.
$comment = $this->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([
'entity_id' => $entity->id(),
'pid' => $pid,
'entity_type' => $entity->getEntityTypeId(),
'field_name' => $field_name,
]);
$build['comment_form'] = $this->entityFormBuilder()->getForm($comment);
return $build;
}
/**
* Access check for the reply form.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity this comment belongs to.
* @param string $field_name
* The field_name to which the comment belongs.
* @param int $pid
* (optional) Some comments are replies to other comments. In those cases,
* $pid is the parent comment's comment ID. Defaults to NULL.
*
* @return \Drupal\Core\Access\AccessResultInterface
* An access result
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function replyFormAccess(EntityInterface $entity, $field_name, $pid = NULL) {
// Check if entity and field exists.
$fields = $this->commentManager->getFields($entity->getEntityTypeId());
if (empty($fields[$field_name])) {
throw new NotFoundHttpException();
}
$account = $this->currentUser();
// Check if the user has the proper permissions.
$access = AccessResult::allowedIfHasPermission($account, 'post comments');
// If commenting is open on the entity.
$status = $entity->{$field_name}->status;
$access = $access->andIf(AccessResult::allowedIf($status == CommentItemInterface::OPEN)
->addCacheableDependency($entity))
// And if user has access to the host entity.
->andIf(AccessResult::allowedIf($entity->access('view')));
// $pid indicates that this is a reply to a comment.
if ($pid) {
// Check if the user has the proper permissions.
$access = $access->andIf(AccessResult::allowedIfHasPermission($account, 'access comments'));
// Load the parent comment.
$comment = $this->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->addCacheableDependency($comment);
}
}
return $access;
}
/**
* Returns a set of nodes' last read timestamps.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request of the page.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The JSON response.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function renderNewCommentsNodeLinks(Request $request) {
if ($this->currentUser()->isAnonymous()) {
throw new AccessDeniedHttpException();
}
$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 = [];
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, $field_name);
$query = $page_number ? ['page' => $page_number] : NULL;
$links[$nid] = [
'new_comment_count' => (int) $new,
'first_new_comment_link' => $this->getUrlGenerator()->generateFromRoute('entity.node.canonical', ['node' => $node->id()], ['query' => $query, 'fragment' => 'new']),
];
}
return new JsonResponse($links);
}
}

View file

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

View file

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

View file

@ -0,0 +1,106 @@
<?php
namespace Drupal\comment\Form;
use Drupal\comment\CommentManagerInterface;
use Drupal\Core\Entity\EntityDeleteForm;
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a confirmation form for deleting a comment type entity.
*
* @internal
*/
class CommentTypeDeleteForm extends EntityDeleteForm {
/**
* The comment manager service.
*
* @var \Drupal\comment\CommentManagerInterface
*/
protected $commentManager;
/**
* 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\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(CommentManagerInterface $comment_manager, EntityManager $entity_manager, LoggerInterface $logger) {
$this->commentManager = $comment_manager;
$this->entityManager = $entity_manager;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$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->entityTypeManager->getStorage('comment')->getQuery()
->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.', [
'%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.', ['%label' => $this->entity->label()]) . '</p>';
}
if ($caption) {
$form['description'] = ['#markup' => $caption];
return $form;
}
else {
return parent::buildForm($form, $form_state);
}
}
}

View file

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

View file

@ -0,0 +1,50 @@
<?php
namespace Drupal\comment\Form;
use Drupal\Core\Entity\ContentEntityDeleteForm;
/**
* Provides the comment delete confirmation form.
*
* @internal
*/
class DeleteForm extends ContentEntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
// Point to the entity of which this comment is a reply.
return $this->entity->get('entity_id')->entity->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('comment')->notice('Deleted comment @cid and its replies.', ['@cid' => $this->entity->id()]);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\comment\Plugin\Action;
use Drupal\Core\Action\Plugin\Action\DeleteAction;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
/**
* Deletes a comment.
*
* @deprecated in Drupal 8.6.x, to be removed before Drupal 9.0.0.
* Use \Drupal\Core\Action\Plugin\Action\DeleteAction instead.
*
* @see \Drupal\Core\Action\Plugin\Action\DeleteAction
* @see https://www.drupal.org/node/2934349
*
* @Action(
* id = "comment_delete_action",
* label = @Translation("Delete comment")
* )
*/
class DeleteComment extends DeleteAction {
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $temp_store_factory, $current_user);
@trigger_error(__NAMESPACE__ . '\DeleteComment is deprecated in Drupal 8.6.x, will be removed before Drupal 9.0.0. Use \Drupal\Core\Action\Plugin\Action\DeleteAction instead. See https://www.drupal.org/node/2934349.', E_USER_DEPRECATED);
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Drupal\comment\Plugin\Action;
use Drupal\Core\Action\Plugin\Action\PublishAction;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Publishes a comment.
*
* @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.0.
* Use \Drupal\Core\Action\Plugin\Action\PublishAction instead.
*
* @see \Drupal\Core\Action\Plugin\Action\PublishAction
* @see https://www.drupal.org/node/2919303
*
* @Action(
* id = "comment_publish_action",
* label = @Translation("Publish comment"),
* type = "comment"
* )
*/
class PublishComment extends PublishAction {
/**
* {@inheritdoc}
*/
public function __construct($configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
@trigger_error(__NAMESPACE__ . '\PublishComment is deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use \Drupal\Core\Action\Plugin\Action\PublishAction instead. See https://www.drupal.org/node/2919303.', E_USER_DEPRECATED);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\comment\Plugin\Action;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Action\Plugin\Action\SaveAction;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Saves a comment.
*
* @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.0.
* Use \Drupal\Core\Action\Plugin\Action\SaveAction instead.
*
* @see \Drupal\Core\Action\Plugin\Action\SaveAction
* @see https://www.drupal.org/node/2919303
*
* @Action(
* id = "comment_save_action",
* label = @Translation("Save comment"),
* type = "comment"
* )
*/
class SaveComment extends SaveAction {
/**
* {@inheritdoc}
*/
public function __construct($configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, TimeInterface $time) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $time);
@trigger_error(__NAMESPACE__ . '\SaveComment is deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use \Drupal\Core\Action\Plugin\Action\SaveAction instead. See https://www.drupal.org/node/2919303.', E_USER_DEPRECATED);
}
}

View file

@ -0,0 +1,128 @@
<?php
namespace Drupal\comment\Plugin\Action;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Entity\EntityViewBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* 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 implements ContainerFactoryPluginInterface {
/**
* The comment entity builder handler.
*
* @var \Drupal\Core\Entity\EntityViewBuilderInterface
*/
protected $viewBuilder;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a UnpublishByKeywordComment 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\Entity\EntityViewBuilderInterface $comment_view_builder
* The comment entity builder handler.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityViewBuilderInterface $comment_view_builder, RendererInterface $renderer) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->viewBuilder = $comment_view_builder;
$this->renderer = $renderer;
}
/**
* {@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')->getViewBuilder('comment'),
$container->get('renderer')
);
}
/**
* {@inheritdoc}
*/
public function execute($comment = NULL) {
$build = $this->viewBuilder->view($comment);
$text = $this->renderer->renderPlain($build);
foreach ($this->configuration['keywords'] as $keyword) {
if (strpos($text, $keyword) !== FALSE) {
$comment->setUnpublished();
$comment->save();
break;
}
}
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'keywords' => [],
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['keywords'] = [
'#title' => $this->t('Keywords'),
'#type' => 'textarea',
'#description' => $this->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,33 @@
<?php
namespace Drupal\comment\Plugin\Action;
use Drupal\Core\Action\Plugin\Action\UnpublishAction;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Unpublishes a comment.
*
* @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.0.
* Use \Drupal\Core\Action\Plugin\Action\UnpublishAction instead.
*
* @see \Drupal\Core\Action\Plugin\Action\UnpublishAction
* @see https://www.drupal.org/node/2919303
*
* @Action(
* id = "comment_unpublish_action",
* label = @Translation("Unpublish comment"),
* type = "comment"
* )
*/
class UnpublishComment extends UnpublishAction {
/**
* {@inheritdoc}
*/
public function __construct($configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
@trigger_error(__NAMESPACE__ . '\UnpublishComment is deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use \Drupal\Core\Action\Plugin\Action\UnpublishAction instead. See https://www.drupal.org/node/2919303.', E_USER_DEPRECATED);
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Drupal\comment\Plugin\EntityReferenceSelection;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
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 DefaultSelection {
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
// Adding the 'comment_access' tag is sadly insufficient for comments:
// core requires us to also know about the concept of 'published' and
// 'unpublished'.
if (!$this->currentUser->hasPermission('administer comments')) {
$query->condition('status', CommentInterface::PUBLISHED);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$comment = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
// In order to create a referenceable comment, it needs to published.
/** @var \Drupal\comment\CommentInterface $comment */
$comment->setPublished();
return $comment;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableNewEntities(array $entities) {
$entities = parent::validateReferenceableNewEntities($entities);
// Mirror the conditions checked in buildEntityQuery().
if (!$this->currentUser->hasPermission('administer comments')) {
$entities = array_filter($entities, function ($comment) {
/** @var \Drupal\comment\CommentInterface $comment */
return $comment->isPublished();
});
}
return $entities;
}
/**
* {@inheritdoc}
*/
public function entityQueryAlter(SelectInterface $query) {
parent::entityQueryAlter($query);
$tables = $query->getTables();
$data_table = 'comment_field_data';
if (!isset($tables['comment_field_data']['alias'])) {
// If no conditions join against the comment data table, it should be
// joined manually to allow node access processing.
$query->innerJoin($data_table, NULL, "base_table.cid = $data_table.cid AND $data_table.default_langcode = 1");
}
// 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 \Drupal\node\Plugin\EntityReferenceSelection\NodeSelection::buildEntityQuery()
if (!$this->currentUser->hasPermission('bypass node access') && !count($this->moduleHandler->getImplementations('node_grants'))) {
$query->condition($node_alias . '.status', 1);
}
}
}

View file

@ -0,0 +1,52 @@
<?php
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, $langcode) {
$elements = [];
foreach ($items as $delta => $item) {
/** @var $comment \Drupal\comment\CommentInterface */
$comment = $item->getEntity();
$account = $comment->getOwner();
$elements[$delta] = [
'#theme' => 'username',
'#account' => $account,
'#cache' => [
'tags' => $account->getCacheTags() + $comment->getCacheTags(),
],
];
}
return $elements;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return $field_definition->getName() === 'name' && $field_definition->getTargetEntityTypeId() === 'comment';
}
}

View file

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

View file

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

View file

@ -0,0 +1,212 @@
<?php
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\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",
* cardinality = 1,
* )
*/
class CommentItem extends FieldItemBase implements CommentItemInterface {
use UrlGeneratorTrait;
/**
* {@inheritdoc}
*/
public static function defaultStorageSettings() {
return [
'comment_type' => '',
] + parent::defaultStorageSettings();
}
/**
* {@inheritdoc}
*/
public static function defaultFieldSettings() {
return [
'default_mode' => CommentManagerInterface::COMMENT_MODE_THREADED,
'per_page' => 50,
'form_location' => CommentItemInterface::FORM_BELOW,
'anonymous' => 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 [
'columns' => [
'status' => [
'description' => 'Whether comments are allowed on this entity: 0 = no, 1 = closed (read only), 2 = open (read/write).',
'type' => 'int',
'default' => 0,
],
],
'indexes' => [],
'foreign keys' => [],
];
}
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
$element = [];
$settings = $this->getSettings();
$anonymous_user = new AnonymousUserSession();
$element['default_mode'] = [
'#type' => 'checkbox',
'#title' => t('Threading'),
'#default_value' => $settings['default_mode'],
'#description' => t('Show comment replies in a threaded list.'),
];
$element['per_page'] = [
'#type' => 'number',
'#title' => t('Comments per page'),
'#default_value' => $settings['per_page'],
'#required' => TRUE,
'#min' => 1,
'#max' => 1000,
];
$element['anonymous'] = [
'#type' => 'select',
'#title' => t('Anonymous commenting'),
'#default_value' => $settings['anonymous'],
'#options' => [
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'] = [
'#type' => 'checkbox',
'#title' => t('Show reply form on the same page as comments'),
'#default_value' => $settings['form_location'],
];
$element['preview'] = [
'#type' => 'radios',
'#title' => t('Preview comment'),
'#default_value' => $settings['preview'],
'#options' => [
DRUPAL_DISABLED => t('Disabled'),
DRUPAL_OPTIONAL => t('Optional'),
DRUPAL_REQUIRED => t('Required'),
],
];
return $element;
}
/**
* {@inheritdoc}
*/
public static function mainPropertyName() {
return 'status';
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
// There is always a value for this field, it is one of
// CommentItemInterface::OPEN, CommentItemInterface::CLOSED or
// CommentItemInterface::HIDDEN.
return FALSE;
}
/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
$element = [];
// @todo Inject entity storage once typed-data supports container injection.
// See https://www.drupal.org/node/2053415 for more details.
$comment_types = CommentType::loadMultiple();
$options = [];
$entity_type = $this->getEntity()->getEntityTypeId();
foreach ($comment_types as $comment_type) {
if ($comment_type->getTargetEntityTypeId() == $entity_type) {
$options[$comment_type->id()] = $comment_type->label();
}
}
$element['comment_type'] = [
'#type' => 'select',
'#title' => t('Comment type'),
'#options' => $options,
'#required' => TRUE,
'#description' => $this->t('Select the Comment type to use for this comment field. Manage the comment types from the <a href=":url">administration overview page</a>.', [':url' => $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,35 @@
<?php
namespace Drupal\comment\Plugin\Field\FieldType;
/**
* Interface definition for Comment items.
*/
interface CommentItemInterface {
/**
* Comments for this entity are hidden.
*/
const HIDDEN = 0;
/**
* Comments for this entity are closed.
*/
const CLOSED = 1;
/**
* Comments for this entity are open.
*/
const OPEN = 2;
/**
* Comment form should be displayed on a separate page.
*/
const FORM_SEPARATE_PAGE = 0;
/**
* Comment form should be shown below post or list of comments.
*/
const FORM_BELOW = 1;
}

View file

@ -0,0 +1,104 @@
<?php
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'] = [
'#type' => 'radios',
'#title' => t('Comments'),
'#title_display' => 'invisible',
'#default_value' => $items->status,
'#options' => [
CommentItemInterface::OPEN => t('Open'),
CommentItemInterface::CLOSED => t('Closed'),
CommentItemInterface::HIDDEN => t('Hidden'),
],
CommentItemInterface::OPEN => [
'#description' => t('Users with the "Post comments" permission can post comments.'),
],
CommentItemInterface::CLOSED => [
'#description' => t('Users cannot post comments, but existing comments will be displayed.'),
],
CommentItemInterface::HIDDEN => [
'#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 += [
'#type' => 'details',
// Open the details when the selected value is different to the stored
// default values for the field.
'#open' => ($items->status != $field_default_values[0]['status']),
'#group' => 'advanced',
'#attributes' => [
'class' => ['comment-' . Html::getClass($entity->getEntityTypeId()) . '-settings-form'],
],
'#attached' => [
'library' => ['comment/drupal.comment'],
],
];
}
return $element;
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
// Add default values for statistics properties because we don't want to
// have them in form.
foreach ($values as &$value) {
$value += [
'cid' => 0,
'last_comment_timestamp' => 0,
'last_comment_name' => '',
'last_comment_uid' => 0,
'comment_count' => 0,
];
}
return $values;
}
}

View file

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

View file

@ -0,0 +1,46 @@
<?php
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,101 @@
<?php
namespace Drupal\comment\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\comment\CommentInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the CommentName constraint.
*/
class CommentNameConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* 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(['name' => $author_name]);
if (!empty($users)) {
$this->context->buildViolation($constraint->messageNameTaken, ['%name' => $author_name])
->atPath('name')
->addViolation();
}
}
// If an author name and owner are given, make sure they match.
elseif (isset($author_name) && $author_name !== '' && $owner_id) {
$owner = $this->userStorage->load($owner_id);
if ($owner->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,47 @@
<?php
namespace Drupal\comment\Plugin\migrate;
use Drupal\migrate_drupal\Plugin\migrate\FieldMigration;
/**
* Migration plugin for Drupal 7 comments with fields.
*/
class D7Comment extends FieldMigration {
/**
* {@inheritdoc}
*/
public function getProcess() {
if ($this->init) {
return parent::getProcess();
}
$this->init = TRUE;
if (!\Drupal::moduleHandler()->moduleExists('field')) {
return parent::getProcess();
}
$definition['source'] = [
'ignore_map' => TRUE,
] + $this->getSourceConfiguration();
$definition['source']['plugin'] = 'd7_field_instance';
$definition['destination']['plugin'] = 'null';
$definition['idMap']['plugin'] = 'null';
$field_migration = $this->migrationPluginManager->createStubMigration($definition);
foreach ($field_migration->getSourcePlugin() as $row) {
$field_name = $row->getSourceProperty('field_name');
$field_type = $row->getSourceProperty('type');
if ($this->fieldPluginManager->hasDefinition($field_type)) {
if (!isset($this->fieldPluginCache[$field_type])) {
$this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($field_type, [], $this);
}
$info = $row->getSource();
$this->fieldPluginCache[$field_type]->defineValueProcessPipeline($this, $field_name, $info);
}
else {
$this->setProcessOfProperty($field_name, $field_name);
}
}
return parent::getProcess();
}
}

View file

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

View file

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

View file

@ -0,0 +1,106 @@
<?php
namespace Drupal\comment\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 comment source from database.
*
* @MigrateSource(
* id = "d6_comment",
* source_module = "comment"
* )
*/
class Comment extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('comments', 'c')
->fields('c', ['cid', 'pid', 'nid', 'uid', 'subject',
'comment', 'hostname', 'timestamp', 'status', 'thread', 'name',
'mail', 'homepage', 'format',
]);
$query->innerJoin('node', 'n', 'c.nid = n.nid');
$query->fields('n', ['type', 'language']);
$query->orderBy('c.timestamp');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
return parent::prepareRow($this->prepareComment($row));
}
/**
* This is a backward compatibility layer for the deprecated migrate source
* plugins d6_comment_variable and d6_comment_variable_per_comment_type.
*
* @param \Drupal\migrate\Row $row
* The row from the source to process.
* @return \Drupal\migrate\Row
* The row object.
* @deprecated in Drupal 8.4.x, to be removed before Drupal 9.0.x.
*/
protected function prepareComment(Row $row) {
if ($this->variableGet('comment_subject_field_' . $row->getSourceProperty('type'), 1)) {
// Comment subject visible.
$row->setSourceProperty('field_name', 'comment');
$row->setSourceProperty('comment_type', 'comment');
}
else {
$row->setSourceProperty('field_name', 'comment_no_subject');
$row->setSourceProperty('comment_type', 'comment_no_subject');
}
// In D6, status=0 means published, while in D8 means the opposite.
// See https://www.drupal.org/node/237636.
$row->setSourceProperty('status', !$row->getSourceProperty('status'));
// If node did not have a language, use site default language as a fallback.
if (!$row->getSourceProperty('language')) {
$language_default = $this->variableGet('language_default', NULL);
$language = $language_default ? $language_default->language : 'en';
$row->setSourceProperty('language', $language);
}
return $row;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'cid' => $this->t('Comment ID.'),
'pid' => $this->t('Parent comment ID. If set to 0, this comment is not a reply to an existing comment.'),
'nid' => $this->t('The {node}.nid to which this comment is a reply.'),
'uid' => $this->t('The {users}.uid who authored the comment. If set to 0, this comment was created by an anonymous user.'),
'subject' => $this->t('The comment title.'),
'comment' => $this->t('The comment body.'),
'hostname' => $this->t("The author's host name."),
'timestamp' => $this->t('The time that the comment was created, or last edited by its author, as a Unix timestamp.'),
'status' => $this->t('The published status of a comment. (0 = Published, 1 = Not Published)'),
'format' => $this->t('The {filter_formats}.format of the comment body.'),
'thread' => $this->t("The vancode representation of the comment's place in a thread."),
'name' => $this->t("The comment author's name. Uses {users}.name if the user is logged in, otherwise uses the value typed into the comment form."),
'mail' => $this->t("The comment author's email address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on."),
'homepage' => $this->t("The comment author's home page address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on."),
'type' => $this->t("The {node}.type to which this comment is a reply."),
'language' => $this->t("The {node}.language to which this comment is a reply. Site default language is used as a fallback if node does not have a language."),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['cid']['type'] = 'integer';
return $ids;
}
}

View file

@ -0,0 +1,114 @@
<?php
namespace Drupal\comment\Plugin\migrate\source\d6;
@trigger_error('CommentVariable is deprecated in Drupal 8.4.x and will be removed before Drupal 9.0.x. Use \Drupal\node\Plugin\migrate\source\d6\NodeType instead.', E_USER_DEPRECATED);
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Plugin\migrate\source\DummyQueryTrait;
/**
* @MigrateSource(
* id = "d6_comment_variable",
* source_module = "comment"
* )
*
* @deprecated in Drupal 8.4.x, to be removed before Drupal 9.0.x. Use
* \Drupal\node\Plugin\migrate\source\d6\NodeType instead.
*/
class CommentVariable extends DrupalSqlBase {
use DummyQueryTrait;
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
return new \ArrayIterator($this->getCommentVariables());
}
/**
* {@inheritdoc}
*/
public function count($refresh = FALSE) {
return count($this->getCommentVariables());
}
/**
* Retrieves the values of the comment variables grouped by node type.
*
* @return array
*/
protected function getCommentVariables() {
$comment_prefixes = array_keys($this->commentPrefixes());
$variables = [];
$node_types = $this->select('node_type', 'nt')
->fields('nt', ['type'])
->execute()
->fetchCol();
foreach ($node_types as $node_type) {
foreach ($comment_prefixes as $prefix) {
$variables[] = $prefix . '_' . $node_type;
}
}
$return = [];
$values = $this->select('variable', 'v')
->fields('v', ['name', 'value'])
->condition('name', $variables, 'IN')
->execute()
->fetchAllKeyed();
foreach ($node_types as $node_type) {
foreach ($comment_prefixes as $prefix) {
$name = $prefix . '_' . $node_type;
if (isset($values[$name])) {
$return[$node_type][$prefix] = unserialize($values[$name]);
}
}
}
// The return key will not be used so move it inside the row. This could
// not be done sooner because otherwise empty rows would be created with
// just the node type in it.
foreach ($return as $node_type => $data) {
$return[$node_type]['node_type'] = $node_type;
$return[$node_type]['comment_type'] = empty($data['comment_subject_field']) ?
'comment_no_subject' : 'comment';
}
return $return;
}
/**
* {@inheritdoc}
*/
public function fields() {
return $this->commentPrefixes() + [
'node_type' => $this->t('The node type'),
'comment_type' => $this->t('The comment type'),
];
}
/**
* Comment related data for fields.
*/
protected function commentPrefixes() {
return [
'comment' => $this->t('Default comment setting'),
'comment_default_mode' => $this->t('Default display mode'),
'comment_default_order' => $this->t('Default display order'),
'comment_default_per_page' => $this->t('Default comments per page'),
'comment_controls' => $this->t('Comment controls'),
'comment_anonymous' => $this->t('Anonymous commenting'),
'comment_subject_field' => $this->t('Comment subject field'),
'comment_preview' => $this->t('Preview comment'),
'comment_form_location' => $this->t('Location of comment submission form'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['node_type']['type'] = 'string';
return $ids;
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Drupal\comment\Plugin\migrate\source\d6;
@trigger_error('CommentVariablePerCommentType is deprecated in Drupal 8.4.x and will be removed before Drupal 9.0.x. Use \Drupal\node\Plugin\migrate\source\d6\NodeType instead.', E_USER_DEPRECATED);
/**
* @MigrateSource(
* id = "d6_comment_variable_per_comment_type",
* source_module = "comment"
* )
*
* @deprecated in Drupal 8.4.x, to be removed before Drupal 9.0.x. Use
* \Drupal\node\Plugin\migrate\source\d6\NodeType instead.
*/
class CommentVariablePerCommentType extends CommentVariable {
/**
* Retrieves the values of the comment variables grouped by comment type.
*
* @return array
*/
protected function getCommentVariables() {
$node_types = parent::getCommentVariables();
// The return key used to separate comment types with hidden subject field.
$return = [];
foreach ($node_types as $node_type => $data) {
// Only 2 comment types depending on subject field visibility.
if (!empty($data['comment_subject_field'])) {
// Default label and description should be set in migration.
$return['comment'] = [
'comment_type' => 'comment',
'label' => $this->t('Default comments'),
'description' => $this->t('Allows commenting on content'),
];
}
else {
// Provide a special comment type with hidden subject field.
$return['comment_no_subject'] = [
'comment_type' => 'comment_no_subject',
'label' => $this->t('Comments without subject field'),
'description' => $this->t('Allows commenting on content, comments without subject field'),
];
}
}
return $return;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'comment_type' => $this->t('The comment type'),
'label' => $this->t('The comment type label'),
'description' => $this->t('The comment type description'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['comment_type']['type'] = 'string';
return $ids;
}
}

View file

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

View file

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

View file

@ -0,0 +1,103 @@
<?php
namespace Drupal\comment\Plugin\migrate\source\d7;
@trigger_error('CommentType is deprecated in Drupal 8.4.x and will be removed before Drupal 9.0.x. Use \Drupal\node\Plugin\migrate\source\d7\NodeType instead.', E_USER_DEPRECATED);
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 comment type source from database.
*
* @MigrateSource(
* id = "d7_comment_type",
* source_module = "comment"
* )
*
* @deprecated in Drupal 8.4.x, to be removed before Drupal 9.0.x. Use
* \Drupal\node\Plugin\migrate\source\d7\NodeType instead.
*/
class CommentType extends DrupalSqlBase {
/**
* A map of D7 node types to their labels.
*
* @var string[]
*/
protected $nodeTypes = [];
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('field_config_instance', 'fci')
->distinct()
->fields('fci', ['bundle'])
->condition('fci.entity_type', 'comment');
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$this->nodeTypes = $this->select('node_type', 'nt')
->fields('nt', ['type', 'name'])
->execute()
->fetchAllKeyed();
return parent::initializeIterator();
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$node_type = substr($row->getSourceProperty('bundle'), 13);
$row->setSourceProperty('node_type', $node_type);
$row->setSourceProperty('default_mode', $this->variableGet("comment_default_mode_$node_type", 1));
$row->setSourceProperty('per_page', $this->variableGet("comment_default_per_page_$node_type", 50));
$row->setSourceProperty('anonymous', $this->variableGet("comment_anonymous_$node_type", FALSE));
$row->setSourceProperty('form_location', $this->variableGet("comment_form_location_$node_type", CommentItemInterface::FORM_BELOW));
$row->setSourceProperty('preview', $this->variableGet("comment_preview_$node_type", TRUE));
$row->setSourceProperty('subject', $this->variableGet("comment_subject_field_$node_type", TRUE));
$label = $this->t('@node_type comment', [
'@node_type' => $this->nodeTypes[$node_type],
]);
$row->setSourceProperty('label', $label);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'label' => $this->t('The label of the comment type.'),
'bundle' => $this->t('Bundle ID of the comment type.'),
'node_type' => $this->t('The node type to which this comment type is attached.'),
'default_mode' => $this->t('Default comment mode.'),
'per_page' => $this->t('Number of comments visible per page.'),
'anonymous' => $this->t('Whether anonymous comments are allowed.'),
'form_location' => $this->t('Location of the comment form.'),
'preview' => $this->t('Whether previews are enabled for the comment type.'),
'subject' => $this->t('Whether a subject field is enabled for the comment type.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'bundle' => [
'type' => 'string',
],
];
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace Drupal\comment\Plugin\views\argument;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\Condition;
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\comment\Plugin\views\argument\UserUid object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Database\Connection $database
* Database Service Object.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container->get('database'));
}
public function title() {
if (!$this->argument) {
$title = \Drupal::config('user.settings')->get('anonymous');
}
else {
$title = $this->database->query('SELECT name FROM {users_field_data} WHERE uid = :uid AND default_langcode = 1', [':uid' => $this->argument])->fetchField();
}
if (empty($title)) {
return $this->t('No user');
}
return $title;
}
protected function defaultActions($which = NULL) {
// Disallow summary views on this argument.
if (!$which) {
$actions = parent::defaultActions();
unset($actions['summary asc']);
unset($actions['summary desc']);
return $actions;
}
if ($which != 'summary asc' && $which != 'summary desc') {
return parent::defaultActions($which);
}
}
public function query($group_by = FALSE) {
$this->ensureMyTable();
// Use the table definition to correctly add this user ID condition.
if ($this->table != 'comment_field_data') {
$subselect = $this->database->select('comment_field_data', 'c');
$subselect->addField('c', 'cid');
$subselect->condition('c.uid', $this->argument);
$entity_id = $this->definition['entity_id'];
$entity_type = $this->definition['entity_type'];
$subselect->where("c.entity_id = $this->tableAlias.$entity_id");
$subselect->condition('c.entity_type', $entity_type);
$condition = (new Condition('OR'))
->condition("$this->tableAlias.uid", $this->argument, '=')
->exists($subselect);
$this->query->addWhere(0, $condition);
}
}
/**
* {@inheritdoc}
*/
public function getSortName() {
return $this->t('Numerical', [], ['context' => 'Sort order']);
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,78 @@
<?php
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'] = ['default' => FALSE];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['teaser'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show teaser-style link'),
'#default_value' => $this->options['teaser'],
'#description' => $this->t('Show the comment link in the form used on standard entity teasers, rather than the full entity form.'),
];
parent::buildOptionsForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function query() {}
/**
* {@inheritdoc}
*/
public function preRender(&$values) {
// Render all nodes, so you can grep the comment links.
$entities = [];
foreach ($values as $row) {
$entity = $row->_entity;
$entities[$entity->id()] = $entity;
}
if ($entities) {
$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::service('renderer')->render($this->build[$entity->id()]['links']['comment__comment']) : '';
}
}

View file

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

View file

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

View file

@ -0,0 +1,80 @@
<?php
namespace Drupal\comment\Plugin\views\field;
use Drupal\user\Entity\User;
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 = [
'table' => 'users_field_data',
'field' => 'uid',
'left_table' => 'comment_entity_statistics',
'left_field' => 'last_comment_uid',
'extra' => [
[
'field' => 'uid',
'operator' => '!=',
'value' => '0',
],
],
];
$join = \Drupal::service('plugin.manager.views.join')->createInstance('standard', $definition);
// nes_user alias so this can work with the sort handler, below.
$this->user_table = $this->query->ensureTable('ces_users', $this->relationship, $join);
$this->field_alias = $this->query->addField(NULL, "COALESCE($this->user_table.name, $this->tableAlias.$this->field)", $this->tableAlias . '_' . $this->field);
$this->user_field = $this->query->addField($this->user_table, 'name');
$this->uid = $this->query->addField($this->tableAlias, 'last_comment_uid');
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['link_to_user'] = ['default' => TRUE];
return $options;
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
if (!empty($this->options['link_to_user'])) {
$account = User::create();
$account->name = $this->getValue($values);
$account->uid = $values->{$this->uid};
$username = [
'#theme' => 'username',
'#account' => $account,
];
return \Drupal::service('renderer')->render($username);
}
else {
return $this->sanitizeValue($this->getValue($values));
}
}
}

View file

@ -0,0 +1,22 @@
<?php
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,26 @@
<?php
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 = [
CommentItemInterface::HIDDEN => $this->t('Hidden'),
CommentItemInterface::CLOSED => $this->t('Closed'),
CommentItemInterface::OPEN => $this->t('Open'),
];
return $this->valueOptions;
}
}

View file

@ -0,0 +1,28 @@
<?php
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,37 @@
<?php
namespace Drupal\comment\Plugin\views\filter;
use Drupal\Core\Database\Query\Condition;
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 = (new Condition('OR'))
->condition("$this->tableAlias.uid", $this->value, $this->operator)
->exists($subselect);
$this->query->addWhere($this->options['group'], $condition);
}
}

View file

@ -0,0 +1,130 @@
<?php
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 = [];
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;
}
$comment->link = $comment->url('canonical', ['absolute' => TRUE]);
$comment->rss_namespaces = [];
$comment->rss_elements = [
[
'key' => 'pubDate',
'value' => gmdate('r', $comment->getCreatedTime()),
],
[
'key' => 'dc:creator',
'value' => $comment->getAuthorName(),
],
[
'key' => 'guid',
'value' => 'comment ' . $comment->id() . ' at ' . $base_url,
'attributes' => ['isPermaLink' => 'false'],
],
];
// The comment gets built and modules add to or modify
// $comment->rss_elements and $comment->rss_namespaces.
$build = $this->entityManager->getViewBuilder('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);
}
$item = new \stdClass();
if ($view_mode != 'title') {
// We render comment contents.
$item->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 = [
'#theme' => $this->themeFunctions(),
'#view' => $this->view,
'#options' => $this->options,
'#row' => $item,
];
return $build;
}
}

View file

@ -0,0 +1,37 @@
<?php
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 = [
'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,22 @@
<?php
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,31 @@
<?php
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();
// See \Drupal\comment\CommentStorage::loadThread() 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,99 @@
<?php
namespace Drupal\comment\Plugin\views\wizard;
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.
*
* @var string
*/
protected $createdColumn = 'created';
/**
* Set default values for the filters.
*/
protected $filters = [
'status_node' => [
'value' => TRUE,
'table' => 'node_field_data',
'field' => 'status',
'plugin_id' => 'boolean',
'relationship' => 'node',
'entity_type' => 'node',
'entity_field' => 'status',
],
];
/**
* {@inheritdoc}
*/
protected function rowStyleOptions() {
$options = [];
$options['entity:comment'] = $this->t('comments');
$options['fields'] = $this->t('fields');
return $options;
}
/**
* {@inheritdoc}
*/
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,409 @@
<?php
namespace Drupal\comment\Tests;
@trigger_error(__NAMESPACE__ . '\CommentTestBase is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Use \Drupal\Tests\comment\Functional\CommentTestBase instead. See http://www.drupal.org/node/2908490', E_USER_DEPRECATED);
use Drupal\comment\Entity\CommentType;
use Drupal\comment\Entity\Comment;
use Drupal\comment\CommentInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\node\Entity\NodeType;
use Drupal\simpletest\WebTestBase;
/**
* Provides setup and helper methods for comment tests.
*
* @deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0.
* Use \Drupal\Tests\comment\Functional\CommentTestBase instead.
*
* @see https://www.drupal.org/node/2908490
*/
abstract class CommentTestBase extends WebTestBase {
use CommentTestTrait;
/**
* Modules to install.
*
* @var array
*/
public static $modules = ['block', 'comment', 'node', 'history', 'field_ui', 'datetime'];
/**
* An administrative user with permission to configure comment settings.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A normal user with permission to post comments.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* A test node to which comments will be posted.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
protected function setUp() {
parent::setUp();
// Create an article content type only if it does not yet exist, so that
// child classes may specify the standard profile.
$types = NodeType::loadMultiple();
if (empty($types['article'])) {
$this->drupalCreateContentType(['type' => 'article', 'name' => t('Article')]);
}
// Create two test users.
$this->adminUser = $this->drupalCreateUser([
'administer content types',
'administer comments',
'administer comment types',
'administer comment fields',
'administer comment display',
'skip comment approval',
'post comments',
'access comments',
// Usernames aren't shown in comment edit form autocomplete unless this
// permission is granted.
'access user profiles',
'access content',
]);
$this->webUser = $this->drupalCreateUser([
'access comments',
'post comments',
'create article content',
'edit own comments',
'skip comment approval',
'access content',
]);
// Create comment field on article.
$this->addDefaultCommentField('node', 'article');
// Create a test node authored by the web user.
$this->node = $this->drupalCreateNode(['type' => 'article', 'promote' => 1, 'uid' => $this->webUser->id()]);
$this->drupalPlaceBlock('local_tasks_block');
}
/**
* Posts a comment.
*
* @param \Drupal\Core\Entity\EntityInterface|null $entity
* Node to post comment on or NULL to post to the previously loaded page.
* @param string $comment
* Comment body.
* @param string $subject
* Comment subject.
* @param string $contact
* Set to NULL for no contact info, TRUE to ignore success checking, and
* array of values to set contact info.
* @param string $field_name
* (optional) Field name through which the comment should be posted.
* Defaults to 'comment'.
*
* @return \Drupal\comment\CommentInterface|null
* The posted comment or NULL when posted comment was not found.
*/
public function postComment($entity, $comment, $subject = '', $contact = NULL, $field_name = 'comment') {
$edit = [];
$edit['comment_body[0][value]'] = $comment;
if ($entity !== NULL) {
$field = FieldConfig::loadByName($entity->getEntityTypeId(), $entity->bundle(), $field_name);
}
else {
$field = FieldConfig::loadByName('node', 'article', $field_name);
}
$preview_mode = $field->getSetting('preview');
// Must get the page before we test for fields.
if ($entity !== NULL) {
$this->drupalGet('comment/reply/' . $entity->getEntityTypeId() . '/' . $entity->id() . '/' . $field_name);
}
// Determine the visibility of subject form field.
if (entity_get_form_display('comment', 'comment', 'default')->getComponent('subject')) {
// Subject input allowed.
$edit['subject[0][value]'] = $subject;
}
else {
$this->assertNoFieldByName('subject[0][value]', '', 'Subject field not found.');
}
if ($contact !== NULL && is_array($contact)) {
$edit += $contact;
}
switch ($preview_mode) {
case DRUPAL_REQUIRED:
// Preview required so no save button should be found.
$this->assertNoFieldByName('op', t('Save'), 'Save button not found.');
$this->drupalPostForm(NULL, $edit, t('Preview'));
// Don't break here so that we can test post-preview field presence and
// function below.
case DRUPAL_OPTIONAL:
$this->assertFieldByName('op', t('Preview'), 'Preview button found.');
$this->assertFieldByName('op', t('Save'), 'Save button found.');
$this->drupalPostForm(NULL, $edit, t('Save'));
break;
case DRUPAL_DISABLED:
$this->assertNoFieldByName('op', t('Preview'), 'Preview button not found.');
$this->assertFieldByName('op', t('Save'), 'Save button found.');
$this->drupalPostForm(NULL, $edit, t('Save'));
break;
}
$match = [];
// Get comment ID
preg_match('/#comment-([0-9]+)/', $this->getURL(), $match);
// Get comment.
if ($contact !== TRUE) {
// If true then attempting to find error message.
if ($subject) {
$this->assertText($subject, 'Comment subject posted.');
}
$this->assertText($comment, 'Comment body posted.');
$this->assertTrue((!empty($match) && !empty($match[1])), 'Comment id found.');
}
if (isset($match[1])) {
\Drupal::entityManager()->getStorage('comment')->resetCache([$match[1]]);
return Comment::load($match[1]);
}
}
/**
* Checks current page for specified comment.
*
* @param \Drupal\comment\CommentInterface $comment
* The comment object.
* @param bool $reply
* Boolean indicating whether the comment is a reply to another comment.
*
* @return bool
* Boolean indicating whether the comment was found.
*/
public function commentExists(CommentInterface $comment = NULL, $reply = FALSE) {
if ($comment) {
$comment_element = $this->cssSelect('.comment-wrapper ' . ($reply ? '.indented ' : '') . '#comment-' . $comment->id() . ' ~ article');
if (empty($comment_element)) {
return FALSE;
}
$comment_title = $comment_element[0]->xpath('div/h3/a');
if (empty($comment_title) || ((string) $comment_title[0]) !== $comment->getSubject()) {
return FALSE;
}
$comment_body = $comment_element[0]->xpath('div/div/p');
if (empty($comment_body) || ((string) $comment_body[0]) !== $comment->comment_body->value) {
return FALSE;
}
return TRUE;
}
else {
return FALSE;
}
}
/**
* Deletes a comment.
*
* @param \Drupal\comment\CommentInterface $comment
* Comment to delete.
*/
public function deleteComment(CommentInterface $comment) {
$this->drupalPostForm('comment/' . $comment->id() . '/delete', [], t('Delete'));
$this->assertText(t('The comment and all its replies have been deleted.'), 'Comment deleted.');
}
/**
* Sets the value governing whether the subject field should be enabled.
*
* @param bool $enabled
* Boolean specifying whether the subject field should be enabled.
*/
public function setCommentSubject($enabled) {
$form_display = entity_get_form_display('comment', 'comment', 'default');
if ($enabled) {
$form_display->setComponent('subject', [
'type' => 'string_textfield',
]);
}
else {
$form_display->removeComponent('subject');
}
$form_display->save();
// Display status message.
$this->pass('Comment subject ' . ($enabled ? 'enabled' : 'disabled') . '.');
}
/**
* Sets the value governing the previewing mode for the comment form.
*
* @param int $mode
* The preview mode: DRUPAL_DISABLED, DRUPAL_OPTIONAL or DRUPAL_REQUIRED.
* @param string $field_name
* (optional) Field name through which the comment should be posted.
* Defaults to 'comment'.
*/
public function setCommentPreview($mode, $field_name = 'comment') {
switch ($mode) {
case DRUPAL_DISABLED:
$mode_text = 'disabled';
break;
case DRUPAL_OPTIONAL:
$mode_text = 'optional';
break;
case DRUPAL_REQUIRED:
$mode_text = 'required';
break;
}
$this->setCommentSettings('preview', $mode, format_string('Comment preview @mode_text.', ['@mode_text' => $mode_text]), $field_name);
}
/**
* Sets the value governing whether the comment form is on its own page.
*
* @param bool $enabled
* TRUE if the comment form should be displayed on the same page as the
* comments; FALSE if it should be displayed on its own page.
* @param string $field_name
* (optional) Field name through which the comment should be posted.
* Defaults to 'comment'.
*/
public function setCommentForm($enabled, $field_name = 'comment') {
$this->setCommentSettings('form_location', ($enabled ? CommentItemInterface::FORM_BELOW : CommentItemInterface::FORM_SEPARATE_PAGE), 'Comment controls ' . ($enabled ? 'enabled' : 'disabled') . '.', $field_name);
}
/**
* Sets the value governing restrictions on anonymous comments.
*
* @param int $level
* The level of the contact information allowed for anonymous comments:
* - 0: No contact information allowed.
* - 1: Contact information allowed but not required.
* - 2: Contact information required.
*/
public function setCommentAnonymous($level) {
$this->setCommentSettings('anonymous', $level, format_string('Anonymous commenting set to level @level.', ['@level' => $level]));
}
/**
* Sets the value specifying the default number of comments per page.
*
* @param int $number
* Comments per page value.
* @param string $field_name
* (optional) Field name through which the comment should be posted.
* Defaults to 'comment'.
*/
public function setCommentsPerPage($number, $field_name = 'comment') {
$this->setCommentSettings('per_page', $number, format_string('Number of comments per page set to @number.', ['@number' => $number]), $field_name);
}
/**
* Sets a comment settings variable for the article content type.
*
* @param string $name
* Name of variable.
* @param string $value
* Value of variable.
* @param string $message
* Status message to display.
* @param string $field_name
* (optional) Field name through which the comment should be posted.
* Defaults to 'comment'.
*/
public function setCommentSettings($name, $value, $message, $field_name = 'comment') {
$field = FieldConfig::loadByName('node', 'article', $field_name);
$field->setSetting($name, $value);
$field->save();
// Display status message.
$this->pass($message);
}
/**
* Checks whether the commenter's contact information is displayed.
*
* @return bool
* Contact info is available.
*/
public function commentContactInfoAvailable() {
return preg_match('/(input).*?(name="name").*?(input).*?(name="mail").*?(input).*?(name="homepage")/s', $this->getRawContent());
}
/**
* Performs the specified operation on the specified comment.
*
* @param \Drupal\comment\CommentInterface $comment
* Comment to perform operation on.
* @param string $operation
* Operation to perform.
* @param bool $approval
* Operation is found on approval page.
*/
public function performCommentOperation(CommentInterface $comment, $operation, $approval = FALSE) {
$edit = [];
$edit['operation'] = $operation;
$edit['comments[' . $comment->id() . ']'] = TRUE;
$this->drupalPostForm('admin/content/comment' . ($approval ? '/approval' : ''), $edit, t('Update'));
if ($operation == 'delete') {
$this->drupalPostForm(NULL, [], t('Delete'));
$this->assertRaw(\Drupal::translation()->formatPlural(1, 'Deleted 1 comment.', 'Deleted @count comments.'), format_string('Operation "@operation" was performed on comment.', ['@operation' => $operation]));
}
else {
$this->assertText(t('The update has been performed.'), format_string('Operation "@operation" was performed on comment.', ['@operation' => $operation]));
}
}
/**
* Gets the comment ID for an unapproved comment.
*
* @param string $subject
* Comment subject to find.
*
* @return int
* Comment id.
*/
public function getUnapprovedComment($subject) {
$this->drupalGet('admin/content/comment/approval');
preg_match('/href="(.*?)#comment-([^"]+)"(.*?)>(' . $subject . ')/', $this->getRawContent(), $match);
return $match[2];
}
/**
* Creates a comment comment type (bundle).
*
* @param string $label
* The comment type label.
*
* @return \Drupal\comment\Entity\CommentType
* Created comment type.
*/
protected function createCommentType($label) {
$bundle = CommentType::create([
'id' => $label,
'label' => $label,
'description' => '',
'target_entity_type_id' => 'node',
]);
$bundle->save();
return $bundle;
}
}

View file

@ -0,0 +1,125 @@
<?php
namespace Drupal\comment\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
/**
* Provides common functionality for the Comment test classes.
*/
trait CommentTestTrait {
/**
* Adds the default comment field to an entity.
*
* Attaches a comment field named 'comment' to the given entity type and
* bundle. Largely replicates the default behavior in Drupal 7 and earlier.
*
* @param string $entity_type
* The entity type to attach the default comment field to.
* @param string $bundle
* The bundle to attach the default comment field to.
* @param string $field_name
* (optional) Field name to use for the comment field. Defaults to
* 'comment'.
* @param int $default_value
* (optional) Default value, one of CommentItemInterface::HIDDEN,
* CommentItemInterface::OPEN, CommentItemInterface::CLOSED. Defaults to
* CommentItemInterface::OPEN.
* @param string $comment_type_id
* (optional) ID of comment type to use. Defaults to 'comment'.
* @param string $comment_view_mode
* (optional) The comment view mode to be used in comment field formatter.
* Defaults to 'full'.
*/
public function addDefaultCommentField($entity_type, $bundle, $field_name = 'comment', $default_value = CommentItemInterface::OPEN, $comment_type_id = 'comment', $comment_view_mode = 'full') {
$entity_manager = \Drupal::entityManager();
// Create the comment type if needed.
$comment_type_storage = $entity_manager->getStorage('comment_type');
if ($comment_type = $comment_type_storage->load($comment_type_id)) {
if ($comment_type->getTargetEntityTypeId() !== $entity_type) {
throw new \InvalidArgumentException("The given comment type id $comment_type_id can only be used with the $entity_type entity type");
}
}
else {
$comment_type_storage->create([
'id' => $comment_type_id,
'label' => Unicode::ucfirst($comment_type_id),
'target_entity_type_id' => $entity_type,
'description' => 'Default comment field',
])->save();
}
// Add a body field to the comment type.
\Drupal::service('comment.manager')->addBodyField($comment_type_id);
// Add a comment field to the host entity type. Create the field storage if
// needed.
if (!array_key_exists($field_name, $entity_manager->getFieldStorageDefinitions($entity_type))) {
$entity_manager->getStorage('field_storage_config')->create([
'entity_type' => $entity_type,
'field_name' => $field_name,
'type' => 'comment',
'translatable' => TRUE,
'settings' => [
'comment_type' => $comment_type_id,
],
])->save();
}
// Create the field if needed, and configure its form and view displays.
if (!array_key_exists($field_name, $entity_manager->getFieldDefinitions($entity_type, $bundle))) {
$entity_manager->getStorage('field_config')->create([
'label' => 'Comments',
'description' => '',
'field_name' => $field_name,
'entity_type' => $entity_type,
'bundle' => $bundle,
'required' => 1,
'default_value' => [
[
'status' => $default_value,
'cid' => 0,
'last_comment_name' => '',
'last_comment_timestamp' => 0,
'last_comment_uid' => 0,
],
],
])->save();
// Entity form displays: assign widget settings for the 'default' form
// mode, and hide the field in all other form modes.
entity_get_form_display($entity_type, $bundle, 'default')
->setComponent($field_name, [
'type' => 'comment_default',
'weight' => 20,
])
->save();
foreach ($entity_manager->getFormModes($entity_type) as $id => $form_mode) {
$display = entity_get_form_display($entity_type, $bundle, $id);
// Only update existing displays.
if ($display && !$display->isNew()) {
$display->removeComponent($field_name)->save();
}
}
// Entity view displays: assign widget settings for the 'default' view
// mode, and hide the field in all other view modes.
entity_get_display($entity_type, $bundle, 'default')
->setComponent($field_name, [
'label' => 'above',
'type' => 'comment_default',
'weight' => 20,
'settings' => ['view_mode' => $comment_view_mode],
])
->save();
foreach ($entity_manager->getViewModes($entity_type) as $id => $view_mode) {
$display = entity_get_display($entity_type, $bundle, $id);
// Only update existing displays.
if ($display && !$display->isNew()) {
$display->removeComponent($field_name)->save();
}
}
}
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Drupal\comment\Tests\Views;
@trigger_error(__NAMESPACE__ . '\CommentTestBase is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Use \Drupal\Tests\comment\Functional\Views\CommentTestBase instead. See http://www.drupal.org/node/2908490', E_USER_DEPRECATED);
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\views\Tests\ViewTestBase;
use Drupal\views\Tests\ViewTestData;
use Drupal\comment\Entity\Comment;
/**
* Provides setup and helper methods for comment views tests.
*
* @deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0.
* Use \Drupal\Tests\comment\Functional\Views\CommentTestBase instead.
*
* @see https://www.drupal.org/node/2908490
*/
abstract class CommentTestBase extends ViewTestBase {
use CommentTestTrait;
/**
* Modules to install.
*
* @var array
*/
public static $modules = ['node', 'comment', 'comment_test_views'];
/**
* A normal user with permission to post comments (without approval).
*
* @var \Drupal\user\UserInterface
*/
protected $account;
/**
* A second normal user that will author a node for $account to comment on.
*
* @var \Drupal\user\UserInterface
*/
protected $account2;
/**
* Stores a node posted by the user created as $account.
*
* @var \Drupal\node\NodeInterface
*/
protected $nodeUserPosted;
/**
* Stores a node posted by the user created as $account2.
*
* @var \Drupal\node\NodeInterface
*/
protected $nodeUserCommented;
/**
* Stores a comment used by the tests.
*
* @var \Drupal\comment\Entity\Comment
*/
protected $comment;
protected function setUp($import_test_views = TRUE) {
parent::setUp($import_test_views);
ViewTestData::createTestViews(get_class($this), ['comment_test_views']);
// Add two users, create a node with the user1 as author and another node
// with user2 as author. For the second node add a comment from user1.
$this->account = $this->drupalCreateUser(['skip comment approval']);
$this->account2 = $this->drupalCreateUser();
$this->drupalLogin($this->account);
$this->drupalCreateContentType(['type' => 'page', 'name' => t('Basic page')]);
$this->addDefaultCommentField('node', 'page');
$this->nodeUserPosted = $this->drupalCreateNode();
$this->nodeUserCommented = $this->drupalCreateNode(['uid' => $this->account2->id()]);
$comment = [
'uid' => $this->loggedInUser->id(),
'entity_id' => $this->nodeUserCommented->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'subject' => 'How much wood would a woodchuck chuck',
'cid' => '',
'pid' => '',
'mail' => 'someone@example.com',
];
$this->comment = Comment::create($comment);
$this->comment->save();
}
}