Commit 3d559225 authored by alexpott's avatar alexpott

Issue #2670730 by chr.fritsch, robpowell, alexpott, tstoeckler, Berdir,...

Issue #2670730 by chr.fritsch, robpowell, alexpott, tstoeckler, Berdir, bojanz, dawehner: Provide a delete action for each content entity type
parent c8dbe3a9
......@@ -9,6 +9,9 @@ add-page:
delete-form:
uri: https://drupal.org/link-relations/delete-form
description: A form where a resource of this type can be deleted.
delete-multiple-form:
uri: https://drupal.org/link-relations/delete-multiple-form
description: A form where multiple resources of this type can be deleted.
revision:
uri: https://drupal.org/link-relations/revision
description: A particular version of this resource.
......
......@@ -1132,6 +1132,11 @@ services:
arguments: ['@entity_type.manager', '@entity_type.bundle.info']
tags:
- { name: access_check, applies_to: _entity_create_any_access }
access_check.entity_delete_multiple:
class: Drupal\Core\Entity\EntityDeleteMultipleAccessCheck
arguments: ['@entity_type.manager', '@tempstore.private', '@request_stack']
tags:
- { name: access_check, applies_to: _entity_delete_multiple_access }
access_check.theme:
class: Drupal\Core\Theme\ThemeAccessCheck
arguments: ['@theme_handler']
......
<?php
namespace Drupal\Core\Action\Plugin\Action;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Redirects to an entity deletion form.
*
* @Action(
* id = "entity:delete_action",
* action_label = @Translation("Delete"),
* deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityDeleteActionDeriver",
* )
*/
class DeleteAction extends EntityActionBase {
/**
* The tempstore object.
*
* @var \Drupal\Core\TempStore\SharedTempStore
*/
protected $tempStore;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new DeleteAction 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\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
$this->currentUser = $current_user;
$this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm');
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('tempstore.private'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function executeMultiple(array $entities) {
/** @var \Drupal\Core\Entity\EntityInterface[] $entities */
$selection = [];
foreach ($entities as $entity) {
$langcode = $entity->language()->getId();
$selection[$entity->id()][$langcode] = $langcode;
}
$this->tempStore->set($this->currentUser->id() . ':' . $this->getPluginDefinition()['type'], $selection);
}
/**
* {@inheritdoc}
*/
public function execute($object = NULL) {
$this->executeMultiple([$object]);
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
return $object->access('delete', $account, $return_as_object);
}
}
......@@ -6,6 +6,8 @@
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -13,6 +15,8 @@
*/
abstract class EntityActionDeriverBase extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
......@@ -20,21 +24,34 @@ abstract class EntityActionDeriverBase extends DeriverBase implements ContainerD
*/
protected $entityTypeManager;
/**
* The string translation service.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $stringTranslation;
/**
* Constructs a new EntityActionDeriverBase object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static($container->get('entity_type.manager'));
return new static(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
......
<?php
namespace Drupal\Core\Action\Plugin\Action\Derivative;
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Provides an action deriver that finds entity types with delete form.
*
* @see \Drupal\Core\Action\Plugin\Action\DeleteAction
*/
class EntityDeleteActionDeriver extends EntityActionDeriverBase {
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
if (empty($this->derivatives)) {
$definitions = [];
foreach ($this->getApplicableEntityTypes() as $entity_type_id => $entity_type) {
$definition = $base_plugin_definition;
$definition['type'] = $entity_type_id;
$definition['label'] = $this->t('Delete @entity_type', ['@entity_type' => $entity_type->getSingularLabel()]);
$definition['confirm_form_route_name'] = 'entity.' . $entity_type->id() . '.delete_multiple_form';
$definitions[$entity_type_id] = $definition;
}
$this->derivatives = $definitions;
}
return $this->derivatives;
}
/**
* {@inheritdoc}
*/
protected function isApplicable(EntityTypeInterface $entity_type) {
return $entity_type->hasLinkTemplate('delete-multiple-form');
}
}
<?php
namespace Drupal\Core\Entity;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Checks if the current user has delete access to the items of the tempstore.
*/
class EntityDeleteMultipleAccessCheck implements AccessInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityTypeManager;
/**
* The tempstore service.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempStore;
/**
* Request stack service.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Constructs a new EntityDeleteMultipleAccessCheck.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, RequestStack $request_stack) {
$this->entityTypeManager = $entity_type_manager;
$this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm');
$this->requestStack = $request_stack;
}
/**
* Checks if the user has delete access for at least one item of the store.
*
* @param \Drupal\Core\Session\AccountInterface $account
* Run access checks for this account.
* @param string $entity_type_id
* Entity type ID.
*
* @return \Drupal\Core\Access\AccessResult
* Allowed or forbidden, neutral if tempstore is empty.
*/
public function access(AccountInterface $account, $entity_type_id) {
if (!$this->requestStack->getCurrentRequest()->getSession()) {
return AccessResult::neutral();
}
$selection = $this->tempStore->get($account->id() . ':' . $entity_type_id);
if (empty($selection) || !is_array($selection)) {
return AccessResult::neutral();
}
$entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple(array_keys($selection));
foreach ($entities as $entity) {
// As long as the user has access to delete one entity allow access to the
// delete form. Access will be checked again in
// Drupal\Core\Entity\Form\DeleteMultipleForm::submit() in case it has
// changed in the meantime.
if ($entity->access('delete', $account)) {
return AccessResult::allowed();
}
}
return AccessResult::forbidden();
}
}
This diff is collapsed.
......@@ -54,4 +54,14 @@ protected function getDeleteFormRoute(EntityTypeInterface $entity_type) {
}
}
/**
* {@inheritdoc}
*/
protected function getDeleteMultipleFormRoute(EntityTypeInterface $entity_type) {
if ($route = parent::getDeleteMultipleFormRoute($entity_type)) {
$route->setOption('_admin_route', TRUE);
return $route;
}
}
}
......@@ -24,6 +24,7 @@
* - edit-form
* - delete-form
* - collection
* - delete-multiple-form
*
* @see \Drupal\Core\Entity\Routing\AdminHtmlRouteProvider.
*/
......@@ -98,6 +99,10 @@ public function getRoutes(EntityTypeInterface $entity_type) {
$collection->add("entity.{$entity_type_id}.collection", $collection_route);
}
if ($delete_multiple_route = $this->getDeleteMultipleFormRoute($entity_type)) {
$collection->add('entity.' . $entity_type->id() . '.delete_multiple_form', $delete_multiple_route);
}
return $collection;
}
......@@ -350,4 +355,23 @@ protected function getEntityTypeIdKeyType(EntityTypeInterface $entity_type) {
return $field_storage_definitions[$entity_type->getKey('id')]->getType();
}
/**
* Returns the delete multiple form route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getDeleteMultipleFormRoute(EntityTypeInterface $entity_type) {
if ($entity_type->hasLinkTemplate('delete-multiple-form') && $entity_type->hasHandlerClass('form', 'delete-multiple-confirm')) {
$route = new Route($entity_type->getLinkTemplate('delete-multiple-form'));
$route->setDefault('_form', $entity_type->getFormClass('delete-multiple-confirm'));
$route->setDefault('entity_type_id', $entity_type->id());
$route->setRequirement('_entity_delete_multiple_access', $entity_type->id());
return $route;
}
}
}
......@@ -148,7 +148,7 @@ public function testBulkForm() {
$errors = $this->xpath('//div[contains(@class, "messages--status")]');
$this->assertFalse($errors, 'No action message shown.');
$this->drupalPostForm(NULL, [], t('Delete'));
$this->assertText(t('Deleted 5 posts.'));
$this->assertText(t('Deleted 5 content items.'));
// Check if we got redirected to the original page.
$this->assertUrl('test_bulk_form');
}
......
......@@ -59,8 +59,18 @@ comment.multiple_delete_confirm:
defaults:
_title: 'Delete'
_form: '\Drupal\comment\Form\ConfirmDeleteMultiple'
entity_type_id: 'comment'
requirements:
_permission: 'administer comments'
_entity_delete_multiple_access: 'comment'
entity.comment.delete_multiple_form:
path: '/admin/content/comment/delete'
defaults:
_title: 'Delete'
_form: '\Drupal\comment\Form\ConfirmDeleteMultiple'
entity_type_id: 'comment'
requirements:
_entity_delete_multiple_access: 'comment'
comment.reply:
path: '/comment/reply/{entity_type}/{entity}/{field_name}/{pid}'
......
......@@ -6,5 +6,5 @@ dependencies:
id: comment_delete_action
label: 'Delete comment'
type: comment
plugin: comment_delete_action
plugin: entity:delete_action:comment
configuration: { }
......@@ -44,6 +44,8 @@ action.configuration.comment_unpublish_action:
type: action_configuration_default
label: 'Unpublish comment configuration'
# @deprecated in Drupal 8.6.x, to be removed before Drupal 9.0.0.
# @see https://www.drupal.org/node/2934349
action.configuration.comment_delete_action:
type: action_configuration_default
label: 'Delete comment configuration'
......
......@@ -56,6 +56,7 @@
* links = {
* "canonical" = "/comment/{comment}",
* "delete-form" = "/comment/{comment}/delete",
* "delete-multiple-form" = "/admin/content/comment/delete",
* "edit-form" = "/comment/{comment}/edit",
* "create" = "/comment",
* },
......
......@@ -290,9 +290,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$info[$comment->id()][$langcode] = $langcode;
}
$this->tempStoreFactory
->get('comment_multiple_delete_confirm')
->set($this->currentUser()->id(), $info);
$form_state->setRedirect('comment.multiple_delete_confirm');
->get('entity_delete_multiple_confirm')
->set($this->currentUser()->id() . ':comment', $info);
$form_state->setRedirect('entity.comment.delete_multiple_form');
}
}
......
......@@ -2,76 +2,21 @@
namespace Drupal\comment\Form;
use Drupal\comment\CommentStorageInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\Form\DeleteMultipleForm as EntityDeleteMultipleForm;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the comment multiple delete confirmation form.
*
* @internal
*/
class ConfirmDeleteMultiple extends ConfirmFormBase {
/**
* The tempstore factory.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempStoreFactory;
/**
* The comment storage.
*
* @var \Drupal\comment\CommentStorageInterface
*/
protected $commentStorage;
/**
* An array of comments to be deleted.
*
* @var string[][]
*/
protected $commentInfo;
/**
* Creates an new ConfirmDeleteMultiple form.
*
* @param \Drupal\comment\CommentStorageInterface $comment_storage
* The comment storage.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
*/
public function __construct(CommentStorageInterface $comment_storage, PrivateTempStoreFactory $temp_store_factory) {
$this->commentStorage = $comment_storage;
$this->tempStoreFactory = $temp_store_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager')->getStorage('comment'),
$container->get('tempstore.private')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'comment_multiple_delete_confirm';
}
class ConfirmDeleteMultiple extends EntityDeleteMultipleForm {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->formatPlural(count($this->commentInfo), '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?');
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?');
}
/**
......@@ -84,116 +29,15 @@ public function getCancelUrl() {
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
protected function getDeletedMessage($count) {
return $this->formatPlural($count, 'Deleted @count comment.', 'Deleted @count comments.');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$this->commentInfo = $this->tempStoreFactory->get('comment_multiple_delete_confirm')->get($this->currentUser()->id());
if (empty($this->commentInfo)) {
return $this->redirect('comment.admin');
}
/** @var \Drupal\comment\CommentInterface[] $comments */
$comments = $this->commentStorage->loadMultiple(array_keys($this->commentInfo));
$items = [];
foreach ($this->commentInfo as $id => $langcodes) {
foreach ($langcodes as $langcode) {
$comment = $comments[$id]->getTranslation($langcode);
$key = $id . ':' . $langcode;
$default_key = $id . ':' . $comment->getUntranslated()->language()->getId();
// If we have a translated entity we build a nested list of translations
// that will be deleted.
$languages = $comment->getTranslationLanguages();
if (count($languages) > 1 && $comment->isDefaultTranslation()) {
$names = [];
foreach ($languages as $translation_langcode => $language) {
$names[] = $language->getName();
unset($items[$id . ':' . $translation_langcode]);
}
$items[$default_key] = [
'label' => [
'#markup' => $this->t('@label (Original translation) - <em>The following comment translations will be deleted:</em>', ['@label' => $comment->label()]),
],
'deleted_translations' => [
'#theme' => 'item_list',
'#items' => $names,
],
];
}
elseif (!isset($items[$default_key])) {
$items[$key] = $comment->label();
}
}
}
$form['comments'] = [
'#theme' => 'item_list',
'#items' => $items,
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getValue('confirm') && !empty($this->commentInfo)) {
$total_count = 0;
$delete_comments = [];
/** @var \Drupal\Core\Entity\ContentEntityInterface[][] $delete_translations */
$delete_translations = [];
/** @var \Drupal\comment\CommentInterface[] $comments */
$comments = $this->commentStorage->loadMultiple(array_keys($this->commentInfo));
foreach ($this->commentInfo as $id => $langcodes) {
foreach ($langcodes as $langcode) {
$comment = $comments[$id]->getTranslation($langcode);
if ($comment->isDefaultTranslation()) {
$delete_comments[$id] = $comment;
unset($delete_translations[$id]);
$total_count += count($comment->getTranslationLanguages());
}
elseif (!isset($delete_comments[$id])) {
$delete_translations[$id][] = $comment;
}
}
}
if ($delete_comments) {
$this->commentStorage->delete($delete_comments);
$this->logger('content')->notice('Deleted @count comments.', ['@count' => count($delete_comments)]);
}
if ($delete_translations) {
$count = 0;
foreach ($delete_translations as $id => $translations) {
$comment = $comments[$id]->getUntranslated();
foreach ($translations as $translation) {
$comment->removeTranslation($translation->language()->getId());
}
$comment->save();
$count += count($translations);
}
if ($count) {
$total_count += $count;
$this->logger('content')->notice('Deleted @count comment translations.', ['@count' => $count]);
}
}
if ($total_count) {
drupal_set_message($this->formatPlural($total_count, 'Deleted 1 comment.', 'Deleted @count comments.'));
}
$this->tempStoreFactory->get('comment_multiple_delete_confirm')->delete($this->currentUser()->id());
}
$form_state->setRedirectUrl($this->getCancelUrl());
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.");
}
}
......@@ -2,97 +2,33 @@
namespace Drupal\comment\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Action\Plugin\Action\DeleteAction;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* 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"),
* type = "comment",
* confirm_form_route_name = "comment.multiple_delete_confirm"
* label = @Translation("Delete comment")
* )
*/
class DeleteComment extends ActionBase implements ContainerFactoryPluginInterface {
/**
* The tempstore object.
*
* @var \Drupal\Core\TempStore\PrivateTempStore
*/
protected $tempStore;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new DeleteComment 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\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
$this->currentUser = $current_user;
$this->tempStore = $temp_store_factory->get('comment_multiple_delete_confirm');