From 2a1c593260745daf64e3120763d23f312ea7aa9c Mon Sep 17 00:00:00 2001 From: Gabor Hojtsy Date: Tue, 6 Mar 2018 12:57:25 +0100 Subject: [PATCH] =?UTF-8?q?Issue=20#2940890=20by=20plach,=20Wim=20Leers,?= =?UTF-8?q?=20effulgentsia,=20matsbla,=20timmillwood:=20Don=E2=80=99t=20al?= =?UTF-8?q?low=20deleting=20revision=20translations=20in=20pending=20revis?= =?UTF-8?q?ions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content_translation.services.yml | 6 + .../Access/ContentTranslationDeleteAccess.php | 124 ++++++++++ .../ContentTranslationManageAccessCheck.php | 50 +++- .../src/ContentTranslationHandler.php | 7 +- .../ContentTranslationController.php | 57 +++-- .../ContentTranslationRouteSubscriber.php | 8 + ...tentTranslationPendingRevisionTestBase.php | 59 +++++ ...slationRevisionTranslationDeletionTest.php | 214 ++++++++++++++++++ 8 files changed, 493 insertions(+), 32 deletions(-) create mode 100644 core/modules/content_translation/src/Access/ContentTranslationDeleteAccess.php create mode 100644 core/modules/content_translation/tests/src/Functional/ContentTranslationRevisionTranslationDeletionTest.php diff --git a/core/modules/content_translation/content_translation.services.yml b/core/modules/content_translation/content_translation.services.yml index 066142fac3f..9a3ba9c0422 100644 --- a/core/modules/content_translation/content_translation.services.yml +++ b/core/modules/content_translation/content_translation.services.yml @@ -9,6 +9,12 @@ services: tags: - { name: event_subscriber } + content_translation.delete_access: + class: Drupal\content_translation\Access\ContentTranslationDeleteAccess + arguments: ['@entity_type.manager', '@content_translation.manager'] + tags: + - { name: access_check, applies_to: _access_content_translation_delete } + content_translation.overview_access: class: Drupal\content_translation\Access\ContentTranslationOverviewAccess arguments: ['@entity.manager'] diff --git a/core/modules/content_translation/src/Access/ContentTranslationDeleteAccess.php b/core/modules/content_translation/src/Access/ContentTranslationDeleteAccess.php new file mode 100644 index 00000000000..5f1933714bc --- /dev/null +++ b/core/modules/content_translation/src/Access/ContentTranslationDeleteAccess.php @@ -0,0 +1,124 @@ +entityTypeManager = $manager; + $this->contentTranslationManager = $content_translation_manager; + } + + /** + * Checks access to translation deletion for the specified route match. + * + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The parameterized route. + * @param \Drupal\Core\Session\AccountInterface $account + * The currently logged in account. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + public function access(RouteMatchInterface $route_match, AccountInterface $account) { + $requirement = $route_match->getRouteObject()->getRequirement('_access_content_translation_delete'); + $entity_type_id = current(explode('.', $requirement)); + $entity = $route_match->getParameter($entity_type_id); + return $this->checkAccess($entity); + } + + /** + * Checks access to translation deletion for the specified entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity translation to be deleted. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + public function checkAccess(ContentEntityInterface $entity) { + $result = AccessResult::allowed(); + + $entity_type_id = $entity->getEntityTypeId(); + $result->addCacheableDependency($entity); + // Add the cache dependencies used by + // ContentTranslationManager::isPendingRevisionSupportEnabled(). + if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { + foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) { + $result->addCacheableDependency($workflow); + } + } + if (!ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle())) { + return $result; + } + + if ($entity->isDefaultTranslation()) { + return $result; + } + + $config = ContentLanguageSettings::load($entity_type_id . '.' . $entity->bundle()); + $result->addCacheableDependency($config); + if (!$this->contentTranslationManager->isEnabled($entity_type_id, $entity->bundle())) { + return $result; + } + + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage($entity_type_id); + $revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $entity->language()->getId()); + if (!$revision_id) { + return $result; + } + + /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */ + $revision = $storage->loadRevision($revision_id); + if ($revision->wasDefaultRevision()) { + return $result; + } + + $result = $result->andIf(AccessResult::forbidden()); + return $result; + } + +} diff --git a/core/modules/content_translation/src/Access/ContentTranslationManageAccessCheck.php b/core/modules/content_translation/src/Access/ContentTranslationManageAccessCheck.php index 6a4be1eb11f..bbf0540e90c 100644 --- a/core/modules/content_translation/src/Access/ContentTranslationManageAccessCheck.php +++ b/core/modules/content_translation/src/Access/ContentTranslationManageAccessCheck.php @@ -3,6 +3,7 @@ namespace Drupal\content_translation\Access; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; @@ -90,15 +91,12 @@ class ContentTranslationManageAccessCheck implements AccessInterface { return AccessResult::allowed()->cachePerPermissions(); } - /* @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */ - $handler = $this->entityManager->getHandler($entity->getEntityTypeId(), 'translation'); - - // Load translation. - $translations = $entity->getTranslationLanguages(); - $languages = $this->languageManager->getLanguages(); - switch ($operation) { case 'create': + /* @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */ + $handler = $this->entityManager->getHandler($entity->getEntityTypeId(), 'translation'); + $translations = $entity->getTranslationLanguages(); + $languages = $this->languageManager->getLanguages(); $source_language = $this->languageManager->getLanguage($source) ?: $entity->language(); $target_language = $this->languageManager->getLanguage($target) ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT); $is_new_translation = ($source_language->getId() != $target_language->getId() @@ -109,12 +107,14 @@ class ContentTranslationManageAccessCheck implements AccessInterface { ->andIf($handler->getTranslationAccess($entity, $operation)); case 'delete': + // @todo Remove this in https://www.drupal.org/node/2945956. + /** @var \Drupal\Core\Access\AccessResultInterface $delete_access */ + $delete_access = \Drupal::service('content_translation.delete_access')->checkAccess($entity); + $access = $this->checkAccess($entity, $language, $operation); + return $delete_access->andIf($access); + case 'update': - $has_translation = isset($languages[$language->getId()]) - && $language->getId() != $entity->getUntranslated()->language()->getId() - && isset($translations[$language->getId()]); - return AccessResult::allowedIf($has_translation)->cachePerPermissions()->addCacheableDependency($entity) - ->andIf($handler->getTranslationAccess($entity, $operation)); + return $this->checkAccess($entity, $language, $operation); } } @@ -122,4 +122,30 @@ class ContentTranslationManageAccessCheck implements AccessInterface { return AccessResult::neutral(); } + /** + * Performs access checks for the specified operation. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being checked. + * @param \Drupal\Core\Language\LanguageInterface $language + * For an update or delete operation, the language code of the translation + * being updated or deleted. + * @param string $operation + * The operation to be checked. + * + * @return \Drupal\Core\Access\AccessResultInterface + * An access result object. + */ + protected function checkAccess(ContentEntityInterface $entity, LanguageInterface $language, $operation) { + /* @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */ + $handler = $this->entityManager->getHandler($entity->getEntityTypeId(), 'translation'); + $translations = $entity->getTranslationLanguages(); + $languages = $this->languageManager->getLanguages(); + $has_translation = isset($languages[$language->getId()]) + && $language->getId() != $entity->getUntranslated()->language()->getId() + && isset($translations[$language->getId()]); + return AccessResult::allowedIf($has_translation)->cachePerPermissions()->addCacheableDependency($entity) + ->andIf($handler->getTranslationAccess($entity, $operation)); + } + } diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php index 9bbfb4c8489..bcf66bd53b0 100644 --- a/core/modules/content_translation/src/ContentTranslationHandler.php +++ b/core/modules/content_translation/src/ContentTranslationHandler.php @@ -390,7 +390,12 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E break; } } - $access = $this->getTranslationAccess($entity, 'delete')->isAllowed() || ($entity->access('delete') && $this->entityType->hasLinkTemplate('delete-form')); + /** @var \Drupal\Core\Access\AccessResultInterface $delete_access */ + $delete_access = \Drupal::service('content_translation.delete_access')->checkAccess($entity); + $access = $delete_access->isAllowed() && ( + $this->getTranslationAccess($entity, 'delete')->isAllowed() || + ($entity->access('delete') && $this->entityType->hasLinkTemplate('delete-form')) + ); $form['actions']['delete_translation'] = [ '#type' => 'submit', '#value' => t('Delete translation'), diff --git a/core/modules/content_translation/src/Controller/ContentTranslationController.php b/core/modules/content_translation/src/Controller/ContentTranslationController.php index 2c9a1036437..3f5891b73a8 100644 --- a/core/modules/content_translation/src/Controller/ContentTranslationController.php +++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php @@ -101,9 +101,9 @@ class ContentTranslationController extends ControllerBase { $rows = []; $show_source_column = FALSE; - $default_revision = $entity; /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ $storage = $this->entityTypeManager()->getStorage($entity_type_id); + $default_revision = $storage->load($entity->id()); if ($this->languageManager()->isMultilingual()) { // Determine whether the current entity is translatable. @@ -131,8 +131,17 @@ class ContentTranslationController extends ControllerBase { // need to load the latest translation-affecting revision for each // language to be sure we are listing all available translations. if ($use_latest_revisions) { + $entity = $default_revision; $latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode); - $entity = $latest_revision_id ? $storage->loadRevision($latest_revision_id) : $default_revision; + if ($latest_revision_id) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $latest_revision */ + $latest_revision = $storage->loadRevision($latest_revision_id); + // Make sure we do not list removed translations, i.e. translations + // that have been part of a default revision but no longer are. + if (!$latest_revision->wasDefaultRevision() || $default_revision->hasTranslation($langcode)) { + $entity = $latest_revision; + } + } $translations = $entity->getTranslationLanguages(); } @@ -227,24 +236,34 @@ class ContentTranslationController extends ControllerBase { $source_name = $this->t('n/a'); } else { - $source_name = isset($languages[$source]) ? $languages[$source]->getName() : $this->t('n/a'); - $delete_access = $entity->access('delete', NULL, TRUE); - $translation_access = $handler->getTranslationAccess($entity, 'delete'); - $cacheability = $cacheability - ->merge(CacheableMetadata::createFromObject($delete_access)) - ->merge(CacheableMetadata::createFromObject($translation_access)); - if ($entity->access('delete') && $entity_type->hasLinkTemplate('delete-form')) { - $links['delete'] = [ - 'title' => $this->t('Delete'), - 'url' => $entity->urlInfo('delete-form'), - 'language' => $language, - ]; + /** @var \Drupal\Core\Access\AccessResultInterface $delete_route_access */ + $delete_route_access = \Drupal::service('content_translation.delete_access')->checkAccess($translation); + $cacheability->addCacheableDependency($delete_route_access); + + if ($delete_route_access->isAllowed()) { + $source_name = isset($languages[$source]) ? $languages[$source]->getName() : $this->t('n/a'); + $delete_access = $entity->access('delete', NULL, TRUE); + $translation_access = $handler->getTranslationAccess($entity, 'delete'); + $cacheability + ->addCacheableDependency($delete_access) + ->addCacheableDependency($translation_access); + + if ($delete_access->isAllowed() && $entity_type->hasLinkTemplate('delete-form')) { + $links['delete'] = [ + 'title' => $this->t('Delete'), + 'url' => $entity->urlInfo('delete-form'), + 'language' => $language, + ]; + } + elseif ($translation_access->isAllowed()) { + $links['delete'] = [ + 'title' => $this->t('Delete'), + 'url' => $delete_url, + ]; + } } - elseif ($translation_access->isAllowed()) { - $links['delete'] = [ - 'title' => $this->t('Delete'), - 'url' => $delete_url, - ]; + else { + $this->messenger()->addWarning($this->t('The "Delete translation" action is only available for published translations.'), FALSE); } } } diff --git a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php index 1947ef0771f..fa1ac3bbf90 100644 --- a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php +++ b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php @@ -164,6 +164,14 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase { ] ); $collection->add("entity.$entity_type_id.content_translation_delete", $route); + + // Add our custom translation deletion access checker. + if ($load_latest_revision) { + $entity_delete_route = $collection->get("entity.$entity_type_id.delete_form"); + if ($entity_delete_route) { + $entity_delete_route->addRequirements(['_access_content_translation_delete' => "$entity_type_id.delete"]); + } + } } } diff --git a/core/modules/content_translation/tests/src/Functional/ContentTranslationPendingRevisionTestBase.php b/core/modules/content_translation/tests/src/Functional/ContentTranslationPendingRevisionTestBase.php index ccbe854d4f4..3a3a058962a 100644 --- a/core/modules/content_translation/tests/src/Functional/ContentTranslationPendingRevisionTestBase.php +++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationPendingRevisionTestBase.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\content_translation\Functional; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; /** @@ -111,4 +112,62 @@ abstract class ContentTranslationPendingRevisionTestBase extends ContentTranslat $this->createContentType(['type' => $this->bundle]); } + /** + * Loads the active revision translation for the specified entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being edited. + * @param string $langcode + * The translation language code. + * + * @return \Drupal\Core\Entity\ContentEntityInterface|null + * The active revision translation or NULL if none could be identified. + */ + protected function loadRevisionTranslation(ContentEntityInterface $entity, $langcode) { + $revision_id = $this->storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode); + /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */ + $revision = $revision_id ? $this->storage->loadRevision($revision_id) : NULL; + return $revision && $revision->hasTranslation($langcode) ? $revision->getTranslation($langcode) : NULL; + } + + /** + * Returns the edit URL for the specified entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being edited. + * + * @return \Drupal\Core\Url + * The edit URL. + */ + protected function getEditUrl(ContentEntityInterface $entity) { + if ($entity->access('update', $this->loggedInUser)) { + $url = $entity->toUrl('edit-form'); + } + else { + $url = $entity->toUrl('drupal:content-translation-edit'); + $url->setRouteParameter('language', $entity->language()->getId()); + } + return $url; + } + + /** + * Returns the delete translation URL for the specified entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being edited. + * + * @return \Drupal\Core\Url + * The delete translation URL. + */ + protected function getDeleteUrl(ContentEntityInterface $entity) { + if ($entity->access('delete', $this->loggedInUser)) { + $url = $entity->toUrl('delete-form'); + } + else { + $url = $entity->toUrl('drupal:content-translation-delete'); + $url->setRouteParameter('language', $entity->language()->getId()); + } + return $url; + } + } diff --git a/core/modules/content_translation/tests/src/Functional/ContentTranslationRevisionTranslationDeletionTest.php b/core/modules/content_translation/tests/src/Functional/ContentTranslationRevisionTranslationDeletionTest.php new file mode 100644 index 00000000000..842d7deda8c --- /dev/null +++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationRevisionTranslationDeletionTest.php @@ -0,0 +1,214 @@ +enableContentModeration(); + } + + /** + * Tests that translation overview handles pending revisions correctly. + */ + public function testOverview() { + $index = 1; + $accounts = [ + $this->rootUser, + $this->editor, + $this->translator, + ]; + foreach ($accounts as $account) { + $this->currentAccount = $account; + $this->doTestOverview($index++); + } + } + + /** + * Performs a test run. + * + * @param int $index + * The test run index. + */ + public function doTestOverview($index) { + $this->drupalLogin($this->currentAccount); + + // Create a test node. + $values = [ + 'title' => "Test $index.1 EN", + 'moderation_state' => 'published', + ]; + $id = $this->createEntity($values, 'en'); + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $this->storage->load($id); + + // Add a draft translation and check that it is available only in the latest + // revision. + $add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add", [ + $entity->getEntityTypeId() => $id, + 'source' => 'en', + 'target' => 'it', + ], + [ + 'language' => ConfigurableLanguage::load('it'), + 'absolute' => FALSE, + ] + ); + $add_translation_href = $add_translation_url->toString(); + $this->drupalGet($add_translation_url); + $edit = [ + 'title[0][value]' => "Test $index.2 IT", + 'moderation_state[0][state]' => 'draft', + ]; + $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); + $entity = $this->storage->loadUnchanged($id); + $this->assertFalse($entity->hasTranslation('it')); + $it_revision = $this->loadRevisionTranslation($entity, 'it'); + $this->assertTrue($it_revision->hasTranslation('it')); + + // Check that translations cannot be deleted in drafts. + $overview_url = $entity->toUrl('drupal:content-translation-overview'); + $this->drupalGet($overview_url); + $it_delete_url = $this->getDeleteUrl($it_revision); + $it_delete_href = $it_delete_url->toString(); + $this->assertSession()->linkByHrefNotExists($it_delete_href); + $warning = 'The "Delete translation" action is only available for published translations.'; + $this->assertSession()->pageTextContains($warning); + $this->drupalGet($this->getEditUrl($it_revision)); + $this->assertSession()->buttonNotExists('Delete translation'); + + // Publish the translation and verify it can be deleted. + $edit = [ + 'title[0][value]' => "Test $index.3 IT", + 'moderation_state[0][state]' => 'published', + ]; + $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); + $entity = $this->storage->loadUnchanged($id); + $this->assertTrue($entity->hasTranslation('it')); + $it_revision = $this->loadRevisionTranslation($entity, 'it'); + $this->assertTrue($it_revision->hasTranslation('it')); + $this->drupalGet($overview_url); + $this->assertSession()->linkByHrefExists($it_delete_href); + $this->assertSession()->pageTextNotContains($warning); + $this->drupalGet($this->getEditUrl($it_revision)); + $this->assertSession()->buttonExists('Delete translation'); + + // Create an English draft and verify the published translation was + // preserved. + $this->drupalLogin($this->editor); + $en_revision = $this->loadRevisionTranslation($entity, 'en'); + $this->drupalGet($this->getEditUrl($en_revision)); + $edit = [ + 'title[0][value]' => "Test $index.4 EN", + 'moderation_state[0][state]' => 'draft', + ]; + $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); + $entity = $this->storage->loadUnchanged($id); + $this->assertTrue($entity->hasTranslation('it')); + $en_revision = $this->loadRevisionTranslation($entity, 'en'); + $this->assertTrue($en_revision->hasTranslation('it')); + $this->drupalLogin($this->currentAccount); + + // Delete the translation and verify that it is actually gone and that it is + // possible to create it again. + $this->drupalGet($it_delete_url); + $this->drupalPostForm(NULL, [], 'Delete Italian translation'); + $entity = $this->storage->loadUnchanged($id); + $this->assertFalse($entity->hasTranslation('it')); + $it_revision = $this->loadRevisionTranslation($entity, 'it'); + $this->assertTrue($it_revision->wasDefaultRevision()); + $this->assertTrue($it_revision->hasTranslation('it')); + $this->assertTrue($it_revision->getRevisionId() < $entity->getRevisionId()); + $this->drupalGet($overview_url); + $this->assertSession()->linkByHrefNotExists($this->getEditUrl($it_revision)->toString()); + $this->assertSession()->linkByHrefExists($add_translation_href); + + // Publish the English draft and verify the translation is not accidentally + // restored. + $this->drupalLogin($this->editor); + $en_revision = $this->loadRevisionTranslation($entity, 'en'); + $this->drupalGet($this->getEditUrl($en_revision)); + $edit = [ + 'title[0][value]' => "Test $index.6 EN", + 'moderation_state[0][state]' => 'published', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $entity = $this->storage->loadUnchanged($id); + $this->assertFalse($entity->hasTranslation('it')); + $this->drupalLogin($this->currentAccount); + + // Create a published translation again and verify it could be deleted. + $this->drupalGet($add_translation_url); + $edit = [ + 'title[0][value]' => "Test $index.7 IT", + 'moderation_state[0][state]' => 'published', + ]; + $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); + $entity = $this->storage->loadUnchanged($id); + $this->assertTrue($entity->hasTranslation('it')); + $it_revision = $this->loadRevisionTranslation($entity, 'it'); + $this->assertTrue($it_revision->hasTranslation('it')); + $this->drupalGet($overview_url); + $this->assertSession()->linkByHrefExists($it_delete_href); + + // Create a translation draft again and verify it cannot be deleted. + $this->drupalGet($this->getEditUrl($it_revision)); + $edit = [ + 'title[0][value]' => "Test $index.8 IT", + 'moderation_state[0][state]' => 'draft', + ]; + $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); + $entity = $this->storage->loadUnchanged($id); + $this->assertTrue($entity->hasTranslation('it')); + $it_revision = $this->loadRevisionTranslation($entity, 'it'); + $this->assertTrue($it_revision->hasTranslation('it')); + $this->drupalGet($overview_url); + $this->assertSession()->linkByHrefNotExists($it_delete_href); + + // Delete the translation draft and verify the translation can be deleted + // again, since the active revision is now a default revision. + $this->drupalLogin($this->editor); + $this->drupalGet($it_revision->toUrl('version-history')); + $revision_deletion_url = Url::fromRoute('node.revision_delete_confirm', [ + 'node' => $id, + 'node_revision' => $it_revision->getRevisionId(), + ], + [ + 'language' => ConfigurableLanguage::load('it'), + 'absolute' => FALSE, + ] + ); + $revision_deletion_href = $revision_deletion_url->toString(); + $this->getSession()->getDriver()->click("//a[@href='$revision_deletion_href']"); + $this->drupalPostForm(NULL, [], 'Delete'); + $this->drupalLogin($this->currentAccount); + $this->drupalGet($overview_url); + $this->assertSession()->linkByHrefExists($it_delete_href); + + // Verify that now the translation can be deleted. + $this->drupalGet($it_delete_url); + $this->drupalPostForm(NULL, [], 'Delete Italian translation'); + $entity = $this->storage->loadUnchanged($id); + $this->assertFalse($entity->hasTranslation('it')); + $it_revision = $this->loadRevisionTranslation($entity, 'it'); + $this->assertTrue($it_revision->wasDefaultRevision()); + $this->assertTrue($it_revision->hasTranslation('it')); + $this->assertTrue($it_revision->getRevisionId() < $entity->getRevisionId()); + $this->drupalGet($overview_url); + $this->assertSession()->linkByHrefNotExists($this->getEditUrl($it_revision)->toString()); + $this->assertSession()->linkByHrefExists($add_translation_href); + } + +}