diff --git a/core/core.link_relation_types.yml b/core/core.link_relation_types.yml index d4ffce3f2f6c7758993a856e18edb5df8a03e777..21cdd6452e8c85004a514c01354de9b6af692da6 100644 --- a/core/core.link_relation_types.yml +++ b/core/core.link_relation_types.yml @@ -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. diff --git a/core/core.services.yml b/core/core.services.yml index 0fc93c146c8bfb52d993d574541c982a4d29d7ac..9e82af7d4b75401e438a0566dbba1b89edfa789f 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -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'] diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php new file mode 100644 index 0000000000000000000000000000000000000000..fcf76a4fbe5f66273b42542ba90f858650802e1a --- /dev/null +++ b/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php @@ -0,0 +1,99 @@ +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); + } + +} diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityActionDeriverBase.php b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityActionDeriverBase.php index 29159c2931115cde69ed587dd81b4ccb0a389a17..7a410d6cc9870ad87d2219ccb13bf6e2f1b64eec 100644 --- a/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityActionDeriverBase.php +++ b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityActionDeriverBase.php @@ -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') + ); } /** diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityDeleteActionDeriver.php b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityDeleteActionDeriver.php new file mode 100644 index 0000000000000000000000000000000000000000..4be4588afb6c1f20fc3d8febb3678bee93260006 --- /dev/null +++ b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityDeleteActionDeriver.php @@ -0,0 +1,40 @@ +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'); + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityDeleteMultipleAccessCheck.php b/core/lib/Drupal/Core/Entity/EntityDeleteMultipleAccessCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..470c1b091504bad9e21a8c3aa0a2de3ffaf7803c --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityDeleteMultipleAccessCheck.php @@ -0,0 +1,86 @@ +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(); + } + +} diff --git a/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php b/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php new file mode 100644 index 0000000000000000000000000000000000000000..aea3291bd430e464a6c6e6e8570ea27690f33a03 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php @@ -0,0 +1,323 @@ + langcodes format. + * + * @var array + */ + protected $selection = []; + + /** + * The entity type definition. + * + * @var \Drupal\Core\Entity\EntityTypeInterface + */ + protected $entityType; + + /** + * Constructs a new DeleteMultiple object. + * + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory + * The tempstore factory. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger service. + */ + public function __construct(AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, MessengerInterface $messenger) { + $this->currentUser = $current_user; + $this->entityTypeManager = $entity_type_manager; + $this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm'); + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('current_user'), + $container->get('entity_type.manager'), + $container->get('tempstore.private'), + $container->get('messenger') + ); + } + + /** + * {@inheritdoc} + */ + public function getBaseFormId() { + return 'entity_delete_multiple_confirm_form'; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + // Get entity type ID from the route because ::buildForm has not yet been + // called. + $entity_type_id = $this->getRouteMatch()->getParameter('entity_type_id'); + return $entity_type_id . '_delete_multiple_confirm_form'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->formatPlural(count($this->selection), 'Are you sure you want to delete this @item?', 'Are you sure you want to delete these @items?', [ + '@item' => $this->entityType->getSingularLabel(), + '@items' => $this->entityType->getPluralLabel(), + ]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + if ($this->entityType->hasLinkTemplate('collection')) { + return new Url('entity.' . $this->entityTypeId . '.collection'); + } + else { + return new Url(''); + } + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL) { + $this->entityTypeId = $entity_type_id; + $this->entityType = $this->entityTypeManager->getDefinition($this->entityTypeId); + $this->selection = $this->tempStore->get($this->currentUser->id() . ':' . $entity_type_id); + if (empty($this->entityTypeId) || empty($this->selection)) { + return new RedirectResponse($this->getCancelUrl() + ->setAbsolute() + ->toString()); + } + + $items = []; + $entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple(array_keys($this->selection)); + foreach ($this->selection as $id => $selected_langcodes) { + $entity = $entities[$id]; + foreach ($selected_langcodes as $langcode) { + $key = $id . ':' . $langcode; + if ($entity instanceof TranslatableInterface) { + $entity = $entity->getTranslation($langcode); + $default_key = $id . ':' . $entity->getUntranslated()->language()->getId(); + + // Build a nested list of translations that will be deleted if the + // entity has multiple translations. + $entity_languages = $entity->getTranslationLanguages(); + if (count($entity_languages) > 1 && $entity->isDefaultTranslation()) { + $names = []; + foreach ($entity_languages as $translation_langcode => $language) { + $names[] = $language->getName(); + unset($items[$id . ':' . $translation_langcode]); + } + $items[$default_key] = [ + 'label' => [ + '#markup' => $this->t('@label (Original translation) - The following @entity_type translations will be deleted:', + [ + '@label' => $entity->label(), + '@entity_type' => $this->entityType->getSingularLabel(), + ]), + ], + 'deleted_translations' => [ + '#theme' => 'item_list', + '#items' => $names, + ], + ]; + } + elseif (!isset($items[$default_key])) { + $items[$key] = $entity->label(); + } + } + elseif (!isset($items[$key])) { + $items[$key] = $entity->label(); + } + } + } + + $form['entities'] = [ + '#theme' => 'item_list', + '#items' => $items, + ]; + $form = parent::buildForm($form, $form_state); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $total_count = 0; + $delete_entities = []; + $delete_translations = []; + $inaccessible_entities = []; + $storage = $this->entityTypeManager->getStorage($this->entityTypeId); + + $entities = $storage->loadMultiple(array_keys($this->selection)); + foreach ($this->selection as $id => $selected_langcodes) { + $entity = $entities[$id]; + if (!$entity->access('delete', $this->currentUser)) { + $inaccessible_entities[] = $entity; + continue; + } + foreach ($selected_langcodes as $langcode) { + if ($entity instanceof TranslatableInterface) { + $entity = $entity->getTranslation($langcode); + // If the entity is the default translation then deleting it will + // delete all the translations. + if ($entity->isDefaultTranslation()) { + $delete_entities[$id] = $entity; + // If there are translations already marked for deletion then remove + // them as they will be deleted anyway. + unset($delete_translations[$id]); + // Update the total count. Since a single delete will delete all + // translations, we need to add the number of translations to the + // count. + $total_count += count($entity->getTranslationLanguages()); + } + // Add the translation to the list of translations to be deleted + // unless the default translation is being deleted. + elseif (!isset($delete_entities[$id])) { + $delete_translations[$id][] = $entity; + } + } + elseif (!isset($delete_entities[$id])) { + $delete_entities[$id] = $entity; + $total_count++; + } + } + } + + if ($delete_entities) { + $storage->delete($delete_entities); + foreach ($delete_entities as $entity) { + $this->logger($entity->getEntityType()->getProvider())->notice('The @entity-type %label has been deleted.', [ + '@entity-type' => $entity->getEntityType()->getLowercaseLabel(), + '%label' => $entity->label(), + ]); + } + } + + if ($delete_translations) { + /** @var \Drupal\Core\Entity\TranslatableInterface[][] $delete_translations */ + foreach ($delete_translations as $id => $translations) { + $entity = $entities[$id]->getUntranslated(); + foreach ($translations as $translation) { + $entity->removeTranslation($translation->language()->getId()); + } + $entity->save(); + foreach ($translations as $translation) { + $this->logger($entity->getEntityType()->getProvider())->notice('The @entity-type %label @language translation has been deleted.', [ + '@entity-type' => $entity->getEntityType()->getLowercaseLabel(), + '%label' => $entity->label(), + '@language' => $translation->language()->getName(), + ]); + } + $total_count += count($translations); + } + } + + if ($total_count) { + $this->messenger->addStatus($this->getDeletedMessage($total_count)); + } + if ($inaccessible_entities) { + $this->messenger->addWarning($this->getInaccessibleMessage(count($inaccessible_entities))); + } + $this->tempStore->delete($this->currentUser->id()); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + + /** + * Returns the message to show the user after an item was deleted. + * + * @param int $count + * Count of deleted translations. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + * The item deleted message. + */ + protected function getDeletedMessage($count) { + return $this->formatPlural($count, 'Deleted @count item.', 'Deleted @count items.'); + } + + /** + * Returns the message to show the user when an item has not been deleted. + * + * @param int $count + * Count of deleted translations. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + * The item inaccessible message. + */ + protected function getInaccessibleMessage($count) { + return $this->formatPlural($count, "@count item has not been deleted because you do not have the necessary permissions.", "@count items have not been deleted because you do not have the necessary permissions."); + } + +} diff --git a/core/lib/Drupal/Core/Entity/Routing/AdminHtmlRouteProvider.php b/core/lib/Drupal/Core/Entity/Routing/AdminHtmlRouteProvider.php index 1abee11ed5892fa9947ab846cb947309c28b45f9..b04fc3ebd3bea1e1eeb3f0bf2e1b54e28cec6439 100644 --- a/core/lib/Drupal/Core/Entity/Routing/AdminHtmlRouteProvider.php +++ b/core/lib/Drupal/Core/Entity/Routing/AdminHtmlRouteProvider.php @@ -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; + } + } + } diff --git a/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php b/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php index fb40fd5b4ac31ec3235f0f35f259b963bc4b8122..0a99b331bf08e8098cb55f89c1c58e5130eba629 100644 --- a/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php +++ b/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php @@ -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; + } + } + } diff --git a/core/modules/action/tests/src/Functional/BulkFormTest.php b/core/modules/action/tests/src/Functional/BulkFormTest.php index 439ffe4d6d46ea5455ca159b4c7b797ca7aff6b3..ec6ca482bca8633df00ab41662f625e09f0d2a8c 100644 --- a/core/modules/action/tests/src/Functional/BulkFormTest.php +++ b/core/modules/action/tests/src/Functional/BulkFormTest.php @@ -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'); } diff --git a/core/modules/comment/comment.routing.yml b/core/modules/comment/comment.routing.yml index 80ee86c7955bd5e189898f606574fad4c1c339a1..6daeace36a8f3b2e04e3697eaeab86e33e1ebe97 100644 --- a/core/modules/comment/comment.routing.yml +++ b/core/modules/comment/comment.routing.yml @@ -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}' diff --git a/core/modules/comment/config/install/system.action.comment_delete_action.yml b/core/modules/comment/config/install/system.action.comment_delete_action.yml index 035299ea12c89e7fcef886b9cc343ec1a0954d08..08f796667e2aeffe8deffc1d302ca4dcf9c845b2 100644 --- a/core/modules/comment/config/install/system.action.comment_delete_action.yml +++ b/core/modules/comment/config/install/system.action.comment_delete_action.yml @@ -6,5 +6,5 @@ dependencies: id: comment_delete_action label: 'Delete comment' type: comment -plugin: comment_delete_action +plugin: entity:delete_action:comment configuration: { } diff --git a/core/modules/comment/config/schema/comment.schema.yml b/core/modules/comment/config/schema/comment.schema.yml index c29088b1a97376482e47e5e3a1b0623c21a8920a..0f6431897b74b634ba74aa87061359100a554f7c 100644 --- a/core/modules/comment/config/schema/comment.schema.yml +++ b/core/modules/comment/config/schema/comment.schema.yml @@ -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' diff --git a/core/modules/comment/src/Entity/Comment.php b/core/modules/comment/src/Entity/Comment.php index 233c752c59f6bcacd327512f20f00dd895eb7e27..5f5a611565a948d55c8eb8ec96ba0fca7649c492 100644 --- a/core/modules/comment/src/Entity/Comment.php +++ b/core/modules/comment/src/Entity/Comment.php @@ -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", * }, diff --git a/core/modules/comment/src/Form/CommentAdminOverview.php b/core/modules/comment/src/Form/CommentAdminOverview.php index 4fe7291ce32a5d0bdad11dc6ec712b832ad91a8b..8a93ecd85697938c514ac1e69ad65ad699d6003a 100644 --- a/core/modules/comment/src/Form/CommentAdminOverview.php +++ b/core/modules/comment/src/Form/CommentAdminOverview.php @@ -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'); } } diff --git a/core/modules/comment/src/Form/ConfirmDeleteMultiple.php b/core/modules/comment/src/Form/ConfirmDeleteMultiple.php index 704439393342f5c3893ace49391c534d7c315457..825b0af6905504da10d26efdbaa982652009a7a0 100644 --- a/core/modules/comment/src/Form/ConfirmDeleteMultiple.php +++ b/core/modules/comment/src/Form/ConfirmDeleteMultiple.php @@ -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) - The following comment translations will be deleted:', ['@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."); } } diff --git a/core/modules/comment/src/Plugin/Action/DeleteComment.php b/core/modules/comment/src/Plugin/Action/DeleteComment.php index 5c6941dd735005584cc5dd8f4e4d5d6818556ec8..ef6116cd2a7e341020c6257f2f5a3ff93e59aab1 100644 --- a/core/modules/comment/src/Plugin/Action/DeleteComment.php +++ b/core/modules/comment/src/Plugin/Action/DeleteComment.php @@ -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'); - parent::__construct($configuration, $plugin_id, $plugin_definition); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('tempstore.private'), - $container->get('current_user') - ); - } - - /** - * {@inheritdoc} - */ - public function executeMultiple(array $entities) { - $info = []; - /** @var \Drupal\comment\CommentInterface $comment */ - foreach ($entities as $comment) { - $langcode = $comment->language()->getId(); - $info[$comment->id()][$langcode] = $langcode; - } - $this->tempStore->set($this->currentUser->id(), $info); - } - - /** - * {@inheritdoc} - */ - public function execute($entity = NULL) { - $this->executeMultiple([$entity]); - } +class DeleteComment extends DeleteAction { /** * {@inheritdoc} */ - public function access($comment, AccountInterface $account = NULL, $return_as_object = FALSE) { - /** @var \Drupal\comment\CommentInterface $comment */ - return $comment->access('delete', $account, $return_as_object); + 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); } } diff --git a/core/modules/node/config/install/system.action.node_delete_action.yml b/core/modules/node/config/install/system.action.node_delete_action.yml index 97662dc50447a31b9ff86d0410d802ebd3c5d0ed..da2d886bf94055f9b7005a0bc151fc7f50b7167a 100644 --- a/core/modules/node/config/install/system.action.node_delete_action.yml +++ b/core/modules/node/config/install/system.action.node_delete_action.yml @@ -6,5 +6,5 @@ dependencies: id: node_delete_action label: 'Delete content' type: node -plugin: node_delete_action +plugin: entity:delete_action:node configuration: { } diff --git a/core/modules/node/config/schema/node.schema.yml b/core/modules/node/config/schema/node.schema.yml index 50b3f3e5ba5e27f25a75cab7e2c4fed1f5992ac8..b6fb7bc2d0ae4f725e214ff3208c01539acab9b0 100644 --- a/core/modules/node/config/schema/node.schema.yml +++ b/core/modules/node/config/schema/node.schema.yml @@ -80,6 +80,8 @@ action.configuration.node_save_action: type: action_configuration_default label: 'Save content configuration' +# @deprecated in Drupal 8.6.x, to be removed before Drupal 9.0.0. +# @see https://www.drupal.org/node/2934349 action.configuration.node_delete_action: type: action_configuration_default label: 'Delete content configuration' diff --git a/core/modules/node/node.routing.yml b/core/modules/node/node.routing.yml index a016e3ac439e56000be415dd77f7578edc592c26..d592bd1a1b18fdb8ee4c41ff968ded912a213f67 100644 --- a/core/modules/node/node.routing.yml +++ b/core/modules/node/node.routing.yml @@ -2,8 +2,17 @@ node.multiple_delete_confirm: path: '/admin/content/node/delete' defaults: _form: '\Drupal\node\Form\DeleteMultiple' + entity_type_id: 'node' requirements: - _permission: 'administer nodes' + _entity_delete_multiple_access: 'node' + +entity.node.delete_multiple_form: + path: '/admin/content/node/delete' + defaults: + _form: '\Drupal\node\Form\DeleteMultiple' + entity_type_id: 'node' + requirements: + _entity_delete_multiple_access: 'node' node.add_page: path: '/node/add' diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index 368ce1b596e5ae2a67449d82ae7aa6f68f1f3680..ccee95b3a663bb5dfa8287bde4dc40f03a45681f 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -33,7 +33,8 @@ * "form" = { * "default" = "Drupal\node\NodeForm", * "delete" = "Drupal\node\Form\NodeDeleteForm", - * "edit" = "Drupal\node\NodeForm" + * "edit" = "Drupal\node\NodeForm", + * "delete-multiple-confirm" = "Drupal\node\Form\DeleteMultiple" * }, * "route_provider" = { * "html" = "Drupal\node\Entity\NodeRouteProvider", @@ -71,6 +72,7 @@ * links = { * "canonical" = "/node/{node}", * "delete-form" = "/node/{node}/delete", + * "delete-multiple-form" = "/admin/content/node/delete", * "edit-form" = "/node/{node}/edit", * "version-history" = "/node/{node}/revisions", * "revision" = "/node/{node}/revisions/{node_revision}/view", diff --git a/core/modules/node/src/Form/DeleteMultiple.php b/core/modules/node/src/Form/DeleteMultiple.php index ef2c89fc917c165eca5329e7f08d1fac805a54f7..a36de5f27f99b6dfaa541827e102718c6316f849 100644 --- a/core/modules/node/src/Form/DeleteMultiple.php +++ b/core/modules/node/src/Form/DeleteMultiple.php @@ -2,78 +2,15 @@ namespace Drupal\node\Form; -use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Form\ConfirmFormBase; -use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Entity\Form\DeleteMultipleForm as EntityDeleteMultipleForm; use Drupal\Core\Url; -use Drupal\Core\TempStore\PrivateTempStoreFactory; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\RedirectResponse; /** * Provides a node deletion confirmation form. * * @internal */ -class DeleteMultiple extends ConfirmFormBase { - - /** - * The array of nodes to delete. - * - * @var string[][] - */ - protected $nodeInfo = []; - - /** - * The tempstore factory. - * - * @var \Drupal\Core\TempStore\PrivateTempStoreFactory - */ - protected $tempStoreFactory; - - /** - * The node storage. - * - * @var \Drupal\Core\Entity\EntityStorageInterface - */ - protected $manager; - - /** - * Constructs a DeleteMultiple form object. - * - * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory - * The tempstore factory. - * @param \Drupal\Core\Entity\EntityManagerInterface $manager - * The entity manager. - */ - public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityManagerInterface $manager) { - $this->tempStoreFactory = $temp_store_factory; - $this->storage = $manager->getStorage('node'); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('tempstore.private'), - $container->get('entity.manager') - ); - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'node_multiple_delete_confirm'; - } - - /** - * {@inheritdoc} - */ - public function getQuestion() { - return $this->formatPlural(count($this->nodeInfo), 'Are you sure you want to delete this item?', 'Are you sure you want to delete these items?'); - } +class DeleteMultiple extends EntityDeleteMultipleForm { /** * {@inheritdoc} @@ -85,117 +22,15 @@ public function getCancelUrl() { /** * {@inheritdoc} */ - public function getConfirmText() { - return t('Delete'); + protected function getDeletedMessage($count) { + return $this->formatPlural($count, 'Deleted @count content item.', 'Deleted @count content items.'); } /** * {@inheritdoc} */ - public function buildForm(array $form, FormStateInterface $form_state) { - $this->nodeInfo = $this->tempStoreFactory->get('node_multiple_delete_confirm')->get(\Drupal::currentUser()->id()); - if (empty($this->nodeInfo)) { - return new RedirectResponse($this->getCancelUrl()->setAbsolute()->toString()); - } - /** @var \Drupal\node\NodeInterface[] $nodes */ - $nodes = $this->storage->loadMultiple(array_keys($this->nodeInfo)); - - $items = []; - foreach ($this->nodeInfo as $id => $langcodes) { - foreach ($langcodes as $langcode) { - $node = $nodes[$id]->getTranslation($langcode); - $key = $id . ':' . $langcode; - $default_key = $id . ':' . $node->getUntranslated()->language()->getId(); - - // If we have a translated entity we build a nested list of translations - // that will be deleted. - $languages = $node->getTranslationLanguages(); - if (count($languages) > 1 && $node->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) - The following content translations will be deleted:', ['@label' => $node->label()]), - ], - 'deleted_translations' => [ - '#theme' => 'item_list', - '#items' => $names, - ], - ]; - } - elseif (!isset($items[$default_key])) { - $items[$key] = $node->label(); - } - } - } - - $form['nodes'] = [ - '#theme' => 'item_list', - '#items' => $items, - ]; - $form = parent::buildForm($form, $form_state); - - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - if ($form_state->getValue('confirm') && !empty($this->nodeInfo)) { - $total_count = 0; - $delete_nodes = []; - /** @var \Drupal\Core\Entity\ContentEntityInterface[][] $delete_translations */ - $delete_translations = []; - /** @var \Drupal\node\NodeInterface[] $nodes */ - $nodes = $this->storage->loadMultiple(array_keys($this->nodeInfo)); - - foreach ($this->nodeInfo as $id => $langcodes) { - foreach ($langcodes as $langcode) { - $node = $nodes[$id]->getTranslation($langcode); - if ($node->isDefaultTranslation()) { - $delete_nodes[$id] = $node; - unset($delete_translations[$id]); - $total_count += count($node->getTranslationLanguages()); - } - elseif (!isset($delete_nodes[$id])) { - $delete_translations[$id][] = $node; - } - } - } - - if ($delete_nodes) { - $this->storage->delete($delete_nodes); - $this->logger('content')->notice('Deleted @count posts.', ['@count' => count($delete_nodes)]); - } - - if ($delete_translations) { - $count = 0; - foreach ($delete_translations as $id => $translations) { - $node = $nodes[$id]->getUntranslated(); - foreach ($translations as $translation) { - $node->removeTranslation($translation->language()->getId()); - } - $node->save(); - $count += count($translations); - } - if ($count) { - $total_count += $count; - $this->logger('content')->notice('Deleted @count content translations.', ['@count' => $count]); - } - } - - if ($total_count) { - drupal_set_message($this->formatPlural($total_count, 'Deleted 1 post.', 'Deleted @count posts.')); - } - - $this->tempStoreFactory->get('node_multiple_delete_confirm')->delete(\Drupal::currentUser()->id()); - } - - $form_state->setRedirect('system.admin_content'); + protected function getInaccessibleMessage($count) { + return $this->formatPlural($count, "@count content item has not been deleted because you do not have the necessary permissions.", "@count content items have not been deleted because you do not have the necessary permissions."); } } diff --git a/core/modules/node/src/Plugin/Action/DeleteNode.php b/core/modules/node/src/Plugin/Action/DeleteNode.php index 19f8a7af2dbf05cffa777c974426ce5e3ce5df95..64fbe120c227ef9d9a4adecae562710083e42079 100644 --- a/core/modules/node/src/Plugin/Action/DeleteNode.php +++ b/core/modules/node/src/Plugin/Action/DeleteNode.php @@ -2,98 +2,33 @@ namespace Drupal\node\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; /** * Redirects to a node deletion form. * + * @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 = "node_delete_action", - * label = @Translation("Delete content"), - * type = "node", - * confirm_form_route_name = "node.multiple_delete_confirm" + * label = @Translation("Delete content") * ) */ -class DeleteNode extends ActionBase implements ContainerFactoryPluginInterface { - - /** - * The tempstore object. - * - * @var \Drupal\Core\TempStore\SharedTempStore - */ - protected $tempStore; - - /** - * The current user. - * - * @var \Drupal\Core\Session\AccountInterface - */ - protected $currentUser; - - /** - * Constructs a new DeleteNode 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\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, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) { - $this->currentUser = $current_user; - $this->tempStore = $temp_store_factory->get('node_multiple_delete_confirm'); - - parent::__construct($configuration, $plugin_id, $plugin_definition); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('tempstore.private'), - $container->get('current_user') - ); - } - - /** - * {@inheritdoc} - */ - public function executeMultiple(array $entities) { - $info = []; - /** @var \Drupal\node\NodeInterface $node */ - foreach ($entities as $node) { - $langcode = $node->language()->getId(); - $info[$node->id()][$langcode] = $langcode; - } - $this->tempStore->set($this->currentUser->id(), $info); - } - - /** - * {@inheritdoc} - */ - public function execute($object = NULL) { - $this->executeMultiple([$object]); - } +class DeleteNode extends DeleteAction { /** * {@inheritdoc} */ - public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { - /** @var \Drupal\node\NodeInterface $object */ - return $object->access('delete', $account, $return_as_object); + 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__ . '\DeleteNode 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); } } diff --git a/core/modules/node/tests/src/Functional/Views/BulkFormTest.php b/core/modules/node/tests/src/Functional/Views/BulkFormTest.php index 319890b6fb8f3d77f82153a211f076d49a4146e9..894a4602db467a2e8b1ba345fc953a0d201c64ba 100644 --- a/core/modules/node/tests/src/Functional/Views/BulkFormTest.php +++ b/core/modules/node/tests/src/Functional/Views/BulkFormTest.php @@ -249,15 +249,15 @@ public function testBulkDeletion() { $this->drupalPostForm(NULL, $edit, t('Apply to selected items')); $label = $this->loadNode(1)->label(); - $this->assertText("$label (Original translation) - The following content translations will be deleted:"); + $this->assertText("$label (Original translation) - The following content item translations will be deleted:"); $label = $this->loadNode(2)->label(); - $this->assertText("$label (Original translation) - The following content translations will be deleted:"); + $this->assertText("$label (Original translation) - The following content item translations will be deleted:"); $label = $this->loadNode(3)->getTranslation('en')->label(); $this->assertText($label); - $this->assertNoText("$label (Original translation) - The following content translations will be deleted:"); + $this->assertNoText("$label (Original translation) - The following content item translations will be deleted:"); $label = $this->loadNode(4)->label(); $this->assertText($label); - $this->assertNoText("$label (Original translation) - The following content translations will be deleted:"); + $this->assertNoText("$label (Original translation) - The following content item translations will be deleted:"); $this->drupalPostForm(NULL, [], t('Delete')); @@ -273,7 +273,7 @@ public function testBulkDeletion() { $node = $this->loadNode(5); $this->assertTrue($node, '5: Node has not been deleted'); - $this->assertText('Deleted 8 posts.'); + $this->assertText('Deleted 8 content items.'); } /** diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php index da13077de8b7d735946aa9b11d61800262dc9a6e..66fa24aa16cef2ccafdacffb9a5b47ab3c12d2b8 100644 --- a/core/modules/system/system.post_update.php +++ b/core/modules/system/system.post_update.php @@ -111,3 +111,22 @@ function system_post_update_change_action_plugins() { } } } + +/** + * Change plugin IDs of delete actions. + */ +function system_post_update_change_delete_action_plugins() { + $old_new_action_id_map = [ + 'comment_delete_action' => 'entity:delete_action:comment', + 'node_delete_action' => 'entity:delete_action:node', + ]; + + /** @var \Drupal\system\Entity\Action[] $actions */ + $actions = \Drupal::entityTypeManager()->getStorage('action')->loadMultiple(); + foreach ($actions as $action) { + if (isset($old_new_action_id_map[$action->getPlugin()->getPluginId()])) { + $action->setPlugin($old_new_action_id_map[$action->getPlugin()->getPluginId()]); + $action->save(); + } + } +} diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevPub.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevPub.php index 862958e92d0a6c5f3b19790b9f001762f31a07f8..8fa69a32b19511b2d6b32860c508bfd7c7450c6b 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevPub.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevPub.php @@ -17,7 +17,8 @@ * "access" = "Drupal\entity_test\EntityTestAccessControlHandler", * "form" = { * "default" = "Drupal\entity_test\EntityTestForm", - * "delete" = "Drupal\entity_test\EntityTestDeleteForm" + * "delete" = "Drupal\entity_test\EntityTestDeleteForm", + * "delete-multiple-confirm" = "Drupal\Core\Entity\Form\DeleteMultipleForm" * }, * "translation" = "Drupal\content_translation\ContentTranslationHandler", * "views_data" = "Drupal\views\EntityViewsData", @@ -45,6 +46,7 @@ * "add-form" = "/entity_test_mulrevpub/add", * "canonical" = "/entity_test_mulrevpub/manage/{entity_test_mulrevpub}", * "delete-form" = "/entity_test/delete/entity_test_mulrevpub/{entity_test_mulrevpub}", + * "delete-multiple-form" = "/entity_test/delete", * "edit-form" = "/entity_test_mulrevpub/manage/{entity_test_mulrevpub}/edit", * "revision" = "/entity_test_mulrevpub/{entity_test_mulrevpub}/revision/{entity_test_mulrevpub_revision}/view", * } diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php index 5aa3ae13d18008a8a791bc2d23733fbbc54d039d..6fd4904a309476e6d27d624eb765c944d6c06d8a 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php @@ -16,7 +16,8 @@ * "view_builder" = "Drupal\entity_test\EntityTestViewBuilder", * "form" = { * "default" = "Drupal\entity_test\EntityTestForm", - * "delete" = "Drupal\entity_test\EntityTestDeleteForm" + * "delete" = "Drupal\entity_test\EntityTestDeleteForm", + * "delete-multiple-confirm" = "Drupal\Core\Entity\Form\DeleteMultipleForm" * }, * "view_builder" = "Drupal\entity_test\EntityTestViewBuilder", * "translation" = "Drupal\content_translation\ContentTranslationHandler", @@ -41,6 +42,7 @@ * "add-form" = "/entity_test_rev/add", * "canonical" = "/entity_test_rev/manage/{entity_test_rev}", * "delete-form" = "/entity_test/delete/entity_test_rev/{entity_test_rev}", + * "delete-multiple-form" = "/entity_test_rev/delete_multiple", * "edit-form" = "/entity_test_rev/manage/{entity_test_rev}/edit", * "revision" = "/entity_test_rev/{entity_test_rev}/revision/{entity_test_rev_revision}/view", * } diff --git a/core/modules/system/tests/src/Functional/Update/UpdateActionsWithEntityPluginsTest.php b/core/modules/system/tests/src/Functional/Update/UpdateActionsWithEntityPluginsTest.php index 4094d00313d8e8b7af69fa1371828d3a27bc79fc..e3bdcf676476aad26ad42ec7c1fd2cec312b4fe2 100644 --- a/core/modules/system/tests/src/Functional/Update/UpdateActionsWithEntityPluginsTest.php +++ b/core/modules/system/tests/src/Functional/Update/UpdateActionsWithEntityPluginsTest.php @@ -53,4 +53,33 @@ public function testUpdateActionsWithEntityPlugins() { } } + /** + * Tests upgrading comment and node delete actions to generic entity ones. + * + * @see system_post_update_change_delete_action_plugins() + */ + public function testUpdateDeleteActionsWithEntityPlugins() { + // comment_delete_actions is not part of the dump files. + $array = [ + 'node_delete_action' => ['node_delete_action', 'entity:delete_action:node'], + ]; + + foreach ($array as $key => list($before, $after)) { + /** @var \Drupal\system\Entity\Action $action */ + $action = Action::load($key); + $this->assertSame($before, $action->getPlugin()->getPluginId()); + } + + $this->runUpdates(); + + foreach ($array as $key => list($before, $after)) { + /** @var \Drupal\system\Entity\Action $action */ + $action = Action::load($key); + $this->assertSame($after, $action->getPlugin()->getPluginId()); + + // Check that the type the action is based on will be a module dependency. + $this->assertArraySubset(['module' => [$action->getPluginDefinition()['type']]], $action->getDependencies()); + } + } + } diff --git a/core/tests/Drupal/FunctionalTests/Entity/DeleteMultipleFormTest.php b/core/tests/Drupal/FunctionalTests/Entity/DeleteMultipleFormTest.php new file mode 100644 index 0000000000000000000000000000000000000000..45f88d680e26a1d85553e5046f5a7d6ec378e840 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Entity/DeleteMultipleFormTest.php @@ -0,0 +1,157 @@ + 'default', + 'label' => 'Default', + ])->save(); + $this->account = $this->drupalCreateUser(['administer entity_test content']); + $this->drupalLogin($this->account); + } + + /** + * Tests the delete form for translatable entities. + */ + public function testTranslatableEntities() { + ConfigurableLanguage::create(['id' => 'es'])->save(); + ConfigurableLanguage::create(['id' => 'fr'])->save(); + + $selection = []; + + $entity1 = EntityTestMulRevPub::create(['type' => 'default', 'name' => 'entity1']); + $entity1->addTranslation('es', ['name' => 'entity1 spanish']); + $entity1->addTranslation('fr', ['name' => 'entity1 french']); + $entity1->save(); + $selection[$entity1->id()]['en'] = 'en'; + + $entity2 = EntityTestMulRevPub::create(['type' => 'default', 'name' => 'entity2']); + $entity2->addTranslation('es', ['name' => 'entity2 spanish']); + $entity2->addTranslation('fr', ['name' => 'entity2 french']); + $entity2->save(); + $selection[$entity2->id()]['es'] = 'es'; + $selection[$entity2->id()]['fr'] = 'fr'; + + $entity3 = EntityTestMulRevPub::create(['type' => 'default', 'name' => 'entity3']); + $entity3->addTranslation('es', ['name' => 'entity3 spanish']); + $entity3->addTranslation('fr', ['name' => 'entity3 french']); + $entity3->save(); + $selection[$entity3->id()]['fr'] = 'fr'; + + // This entity will be inaccessible because of + // Drupal\entity_test\EntityTestAccessControlHandler. + $entity4 = EntityTestMulRevPub::create(['type' => 'default', 'name' => 'forbid_access']); + $entity4->save(); + $selection[$entity4->id()]['en'] = 'en'; + + // Add the selection to the tempstore just like DeleteAction would. + $tempstore = \Drupal::service('tempstore.private')->get('entity_delete_multiple_confirm'); + $tempstore->set($this->account->id() . ':entity_test_mulrevpub', $selection); + + $this->drupalGet('/entity_test/delete'); + $assert = $this->assertSession(); + $assert->statusCodeEquals(200); + $assert->elementTextContains('css', '.page-title', 'Are you sure you want to delete these test entity - revisions, data table, and published interface entities?'); + $list_selector = '#entity-test-mulrevpub-delete-multiple-confirm-form > div.item-list > ul'; + $assert->elementTextContains('css', $list_selector, 'entity1 (Original translation) - The following test entity - revisions, data table, and published interface translations will be deleted:'); + $assert->elementTextContains('css', $list_selector, 'entity2 spanish'); + $assert->elementTextContains('css', $list_selector, 'entity2 french'); + $assert->elementTextNotContains('css', $list_selector, 'entity3 spanish'); + $assert->elementTextContains('css', $list_selector, 'entity3 french'); + $delete_button = $this->getSession()->getPage()->findButton('Delete'); + $delete_button->click(); + $assert = $this->assertSession(); + $assert->addressEquals('/user/' . $this->account->id()); + $assert->responseContains('Deleted 6 items.'); + $assert->responseContains('1 item has not been deleted because you do not have the necessary permissions.'); + + \Drupal::entityTypeManager()->getStorage('entity_test_mulrevpub')->resetCache(); + $remaining_entities = EntityTestMulRevPub::loadMultiple([$entity1->id(), $entity2->id(), $entity3->id(), $entity4->id()]); + $this->assertCount(3, $remaining_entities); + } + + /** + * Tests the delete form for untranslatable entities. + */ + public function testUntranslatableEntities() { + $selection = []; + + $entity1 = EntityTestRev::create(['type' => 'default', 'name' => 'entity1']); + $entity1->save(); + $selection[$entity1->id()]['en'] = 'en'; + + $entity2 = EntityTestRev::create(['type' => 'default', 'name' => 'entity2']); + $entity2->save(); + $selection[$entity2->id()]['en'] = 'en'; + + // This entity will be inaccessible because of + // Drupal\entity_test\EntityTestAccessControlHandler. + $entity3 = EntityTestRev::create(['type' => 'default', 'name' => 'forbid_access']); + $entity3->save(); + $selection[$entity3->id()]['en'] = 'en'; + + // This entity will be inaccessible because of + // Drupal\entity_test\EntityTestAccessControlHandler. + $entity4 = EntityTestRev::create(['type' => 'default', 'name' => 'forbid_access']); + $entity4->save(); + $selection[$entity4->id()]['en'] = 'en'; + + // Add the selection to the tempstore just like DeleteAction would. + $tempstore = \Drupal::service('tempstore.private')->get('entity_delete_multiple_confirm'); + $tempstore->set($this->account->id() . ':entity_test_rev', $selection); + + $this->drupalGet('/entity_test_rev/delete_multiple'); + $assert = $this->assertSession(); + $assert->statusCodeEquals(200); + $assert->elementTextContains('css', '.page-title', 'Are you sure you want to delete these test entity - revisions entities?'); + $list_selector = '#entity-test-rev-delete-multiple-confirm-form > div.item-list > ul'; + $assert->elementTextContains('css', $list_selector, 'entity1'); + $assert->elementTextContains('css', $list_selector, 'entity2'); + $delete_button = $this->getSession()->getPage()->findButton('Delete'); + $delete_button->click(); + $assert = $this->assertSession(); + $assert->addressEquals('/user/' . $this->account->id()); + $assert->responseContains('Deleted 2 items.'); + $assert->responseContains('2 items have not been deleted because you do not have the necessary permissions.'); + + \Drupal::entityTypeManager()->getStorage('entity_test_mulrevpub')->resetCache(); + $remaining_entities = EntityTestRev::loadMultiple([$entity1->id(), $entity2->id(), $entity3->id(), $entity4->id()]); + $this->assertCount(2, $remaining_entities); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Action/DeleteActionTest.php b/core/tests/Drupal/KernelTests/Core/Action/DeleteActionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e5a2bd95dab783732ec16fe21d7b4fd85d82e732 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Action/DeleteActionTest.php @@ -0,0 +1,85 @@ +installEntitySchema('entity_test_mulrevpub'); + $this->installEntitySchema('user'); + $this->installSchema('system', ['sequences', 'key_value_expire']); + + $this->testUser = User::create([ + 'name' => 'foobar', + 'mail' => 'foobar@example.com', + ]); + $this->testUser->save(); + \Drupal::service('current_user')->setAccount($this->testUser); + } + + /** + * @covers \Drupal\Core\Action\Plugin\Action\Derivative\EntityDeleteActionDeriver::getDerivativeDefinitions + */ + public function testGetDerivativeDefinitions() { + $deriver = new EntityDeleteActionDeriver(\Drupal::entityTypeManager(), \Drupal::translation()); + $this->assertEquals([ + 'entity_test_mulrevpub' => [ + 'type' => 'entity_test_mulrevpub', + 'label' => 'Delete test entity - revisions, data table, and published interface', + 'action_label' => 'Delete', + 'confirm_form_route_name' => 'entity.entity_test_mulrevpub.delete_multiple_form', + ], + 'entity_test_rev' => [ + 'type' => 'entity_test_rev', + 'label' => 'Delete test entity - revisions', + 'action_label' => 'Delete', + 'confirm_form_route_name' => 'entity.entity_test_rev.delete_multiple_form', + ], + ], $deriver->getDerivativeDefinitions([ + 'action_label' => 'Delete', + ])); + } + + /** + * @covers \Drupal\Core\Action\Plugin\Action\DeleteAction::execute + */ + public function testDeleteAction() { + $entity = EntityTestMulRevPub::create(['name' => 'test']); + $entity->save(); + + $action = Action::create([ + 'id' => 'entity_delete_action', + 'plugin' => 'entity:delete_action:entity_test_mulrevpub', + ]); + $action->save(); + + $action->execute([$entity]); + $this->assertArraySubset(['module' => ['entity_test']], $action->getDependencies()); + + /** @var \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store */ + $temp_store = \Drupal::service('tempstore.private'); + $store_entries = $temp_store->get('entity_delete_multiple_confirm')->get($this->testUser->id() . ':entity_test_mulrevpub'); + $this->assertArraySubset([$this->testUser->id() => ['en' => 'en']], $store_entries); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Action/PublishActionTest.php b/core/tests/Drupal/KernelTests/Core/Action/PublishActionTest.php index 04bf1c053e39ccb044c59362306b8bac3936456e..d381de115daa9c7e4fad6d127bbdbec275d82182 100644 --- a/core/tests/Drupal/KernelTests/Core/Action/PublishActionTest.php +++ b/core/tests/Drupal/KernelTests/Core/Action/PublishActionTest.php @@ -29,7 +29,7 @@ protected function setUp() { * @covers \Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver::getDerivativeDefinitions */ public function testGetDerivativeDefinitions() { - $deriver = new EntityPublishedActionDeriver(\Drupal::entityTypeManager()); + $deriver = new EntityPublishedActionDeriver(\Drupal::entityTypeManager(), \Drupal::translation()); $this->assertArraySubset([ 'entity_test_mulrevpub' => [ 'type' => 'entity_test_mulrevpub', diff --git a/core/tests/Drupal/KernelTests/Core/Action/SaveActionTest.php b/core/tests/Drupal/KernelTests/Core/Action/SaveActionTest.php index 118f5428d8cbb96333f8336c07975f6de6c5c43f..ed9b2873a3f95e352a09d41630271517fb2cd903 100644 --- a/core/tests/Drupal/KernelTests/Core/Action/SaveActionTest.php +++ b/core/tests/Drupal/KernelTests/Core/Action/SaveActionTest.php @@ -29,7 +29,7 @@ protected function setUp() { * @covers \Drupal\Core\Action\Plugin\Action\Derivative\EntityChangedActionDeriver::getDerivativeDefinitions */ public function testGetDerivativeDefinitions() { - $deriver = new EntityChangedActionDeriver(\Drupal::entityTypeManager()); + $deriver = new EntityChangedActionDeriver(\Drupal::entityTypeManager(), \Drupal::translation()); $this->assertArraySubset([ 'entity_test_mul_changed' => [ 'type' => 'entity_test_mul_changed', diff --git a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php index f5aa40a6ef03e9513063089d5383d6f590dffcc2..e71d7dfcbd9e8e0ab607286cffe48cb6030eaa89 100644 --- a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php +++ b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php @@ -325,6 +325,8 @@ public static function getSkippedDeprecations() { 'The "session_handler.write_check" service relies on the deprecated "Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler" class. It should either be deprecated or its implementation upgraded.', 'Not setting the strict option of the Choice constraint to true is deprecated since Symfony 3.4 and will throw an exception in 4.0.', 'Using the Yaml::PARSE_KEYS_AS_STRINGS flag is deprecated since Symfony 3.4 as it will be removed in 4.0. Quote your keys when they are evaluable instead.', + 'Drupal\node\Plugin\Action\DeleteNode 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.', + 'Drupal\comment\Plugin\Action\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.', ]; }