From 4bd6399b702240c37dec9970721366dd6bd49348 Mon Sep 17 00:00:00 2001 From: Chris Green Date: Tue, 30 Jan 2024 13:04:24 -0700 Subject: [PATCH 1/2] Close #2413769 Prevent the deletion of a bundle entity if there are existing content entities of that bundle --- .../Config/Entity/ConfigEntityBundleBase.php | 31 +++++++++++++++++++ .../Core/Entity/ContentEntityDeleteForm.php | 19 +++++++++--- .../Core/Entity/EntityDeleteFormTrait.php | 19 +++++++++--- .../Core/Entity/Form/DeleteMultipleForm.php | 31 ++++++++++++++----- .../src/Functional/ContactStorageTest.php | 15 +++++++++ .../shortcut/src/Entity/ShortcutSet.php | 2 +- .../taxonomy/src/Entity/Vocabulary.php | 3 +- 7 files changed, 102 insertions(+), 18 deletions(-) diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBundleBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBundleBase.php index 1206c907c2b2..00139c406a4c 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBundleBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBundleBase.php @@ -3,6 +3,7 @@ namespace Drupal\Core\Config\Entity; use Drupal\Core\Config\ConfigNameException; +use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\EntityStorageInterface; /** @@ -53,6 +54,36 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { } } + /** + * {@inheritdoc} + */ + public static function preDelete(EntityStorageInterface $storage, array $entities) { + // Prevent the deletion of bundle entities if there are existing content + // entities of this bundle. + $entity_type_manager = \Drupal::entityTypeManager(); + foreach ($entities as $entity) { + if ($bundle_of = $entity->getEntityType()->getBundleOf()) { + $bundle_of_entity_type = $entity_type_manager->getDefinition($bundle_of); + $storage = $entity_type_manager->getStorage($bundle_of); + + $has_data = (bool) $storage->getQuery() + ->condition($bundle_of_entity_type->getKey('bundle'), $entity->id(), '=') + ->accessCheck(FALSE) + ->range(0, 1) + ->execute(); + + if ($has_data) { + // Use the 409 (Conflict) status code to indicate that the deletion + // could not be completed due to a conflict with the current state of + // the target resource. + throw new EntityStorageException("The '{$entity->label()}' bundle contains data and can not be deleted.", 409); + } + } + } + + parent::preDelete($storage, $entities); + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php b/core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php index 45328bb0c0e3..afd566a056f9 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php @@ -68,12 +68,23 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $form_state->setRedirectUrl($untranslated_entity->toUrl('canonical')); } else { - $entity->delete(); + try { + $entity->delete(); + $this->messenger()->addStatus($message); + $this->logDeletionMessage(); + } + catch (EntityStorageException $e) { + // The 409 (Conflict) status code indicates that the deletion could not be + // completed due to a conflict with the current state of the entity. + if ($e->getCode() == 409) { + $this->messenger()->addError($e->getMessage()); + } + else { + throw $e; + } + } $form_state->setRedirectUrl($this->getRedirectUrl()); } - - $this->messenger()->addStatus($message); - $this->logDeletionMessage(); } /** diff --git a/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php b/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php index 8eb8ed04b876..6c03ed654dc6 100644 --- a/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php +++ b/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php @@ -119,10 +119,21 @@ protected function logDeletionMessage() { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - $this->getEntity()->delete(); - $this->messenger()->addStatus($this->getDeletionMessage()); - $form_state->setRedirectUrl($this->getCancelUrl()); - $this->logDeletionMessage(); + try { + $this->getEntity()->delete(); + $this->messenger()->addStatus($this->getDeletionMessage()); + $this->logDeletionMessage(); + } + catch (EntityStorageException $e) { + // The 409 (Conflict) status code indicates that the deletion could not be + // completed due to a conflict with the current state of the entity. + if ($e->getCode() == 409) { + $this->messenger()->addStatus($e->getMessage()); + } + else { + throw $e; + } + } } } diff --git a/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php b/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php index c24b539a0cd7..10e3295ed4eb 100644 --- a/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php +++ b/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Entity\Form; +use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\BaseFormIdInterface; use Drupal\Core\Form\ConfirmFormBase; @@ -256,14 +257,30 @@ public function submitForm(array &$form, FormStateInterface $form_state) { } if ($delete_entities) { - $storage->delete($delete_entities); - foreach ($delete_entities as $entity) { - $this->logger($entity->getEntityType()->getProvider())->info('The @entity-type %label has been deleted.', [ - '@entity-type' => $entity->getEntityType()->getSingularLabel(), - '%label' => $entity->label(), - ]); + try { + $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()->getSingularLabel(), + '%label' => $entity->label(), + ]); + } } - } + catch (EntityStorageException $e) { + // The 409 (Conflict) status code indicates that the deletion could not be + // completed due to a conflict with the current state of the entity. + if ($e->getCode() == 409) { + $this->messenger()->addError($this->t('Failed deletion of entity %entity_label of exception: %exception', [ + '%entity_label' => $entity->label(), + '%exception' => $e->getMessage(), + ])); + } + else { + throw $e; + } + } + } if ($delete_translations) { /** @var \Drupal\Core\Entity\TranslatableInterface[][] $delete_translations */ diff --git a/core/modules/contact/tests/src/Functional/ContactStorageTest.php b/core/modules/contact/tests/src/Functional/ContactStorageTest.php index a0937614cdae..957b28b43875 100644 --- a/core/modules/contact/tests/src/Functional/ContactStorageTest.php +++ b/core/modules/contact/tests/src/Functional/ContactStorageTest.php @@ -78,4 +78,19 @@ public function testContactStorage() { $this->assertEquals($id, $config->get('id')); } + /** + * Deletes all forms. + */ + public function deleteContactForms() { + // This test uses the contact_storage_test module, which overrides the + // default NULL entity storage handler for contact messages, so we need to + // manually delete all contact messages before deleting the contact forms. + $messages = Message::loadMultiple(); + \Drupal::entityTypeManager() + ->getStorage('contact_message') + ->delete($messages); + + parent::deleteContactForms(); + } + } diff --git a/core/modules/shortcut/src/Entity/ShortcutSet.php b/core/modules/shortcut/src/Entity/ShortcutSet.php index 2bd9713633e2..4f45704221d8 100644 --- a/core/modules/shortcut/src/Entity/ShortcutSet.php +++ b/core/modules/shortcut/src/Entity/ShortcutSet.php @@ -90,7 +90,6 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { * {@inheritdoc} */ public static function preDelete(EntityStorageInterface $storage, array $entities) { - parent::preDelete($storage, $entities); foreach ($entities as $entity) { $storage->deleteAssignedShortcutSets($entity); @@ -105,6 +104,7 @@ public static function preDelete(EntityStorageInterface $storage, array $entitie $entities = $controller->loadMultiple($shortcut_ids); $controller->delete($entities); } + parent::preDelete($storage, $entities); } /** diff --git a/core/modules/taxonomy/src/Entity/Vocabulary.php b/core/modules/taxonomy/src/Entity/Vocabulary.php index aec8c8f0e3c6..54a2847dbde8 100644 --- a/core/modules/taxonomy/src/Entity/Vocabulary.php +++ b/core/modules/taxonomy/src/Entity/Vocabulary.php @@ -108,12 +108,11 @@ public function getDescription() { * {@inheritdoc} */ public static function preDelete(EntityStorageInterface $storage, array $entities) { - parent::preDelete($storage, $entities); - // Only load terms without a parent, child terms will get deleted too. $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term'); $terms = $term_storage->loadMultiple($storage->getToplevelTids(array_keys($entities))); $term_storage->delete($terms); + parent::preDelete($storage, $entities); } /** -- GitLab From 7a15d166d042df86e493bd9c6f40ce4d0d51549e Mon Sep 17 00:00:00 2001 From: Chris Green Date: Tue, 30 Jan 2024 13:10:16 -0700 Subject: [PATCH 2/2] Add back ->setRedirectUrl(->getCancelUrl()); --- core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php | 1 + 1 file changed, 1 insertion(+) diff --git a/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php b/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php index 6c03ed654dc6..08315d20c466 100644 --- a/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php +++ b/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php @@ -122,6 +122,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { try { $this->getEntity()->delete(); $this->messenger()->addStatus($this->getDeletionMessage()); + $form_state->setRedirectUrl($this->getCancelUrl()); $this->logDeletionMessage(); } catch (EntityStorageException $e) { -- GitLab