diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php index 5b4d8dbdb952adddac3829d27d0fa5a408cc2e36..f89a716a3fa958322ffeda9d9f09b167e3c4c9ba 100644 --- a/core/modules/taxonomy/src/Entity/Term.php +++ b/core/modules/taxonomy/src/Entity/Term.php @@ -32,7 +32,12 @@ * "views_data" = "Drupal\taxonomy\TermViewsData", * "form" = { * "default" = "Drupal\taxonomy\TermForm", - * "delete" = "Drupal\taxonomy\Form\TermDeleteForm" + * "delete" = "Drupal\taxonomy\Form\TermDeleteForm", + * "revision-delete" = \Drupal\Core\Entity\Form\RevisionDeleteForm::class, + * "revision-revert" = \Drupal\Core\Entity\Form\RevisionRevertForm::class, + * }, + * "route_provider" = { + * "revision" = \Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider::class, * }, * "translation" = "Drupal\taxonomy\TermTranslationHandler" * }, @@ -40,6 +45,7 @@ * data_table = "taxonomy_term_field_data", * revision_table = "taxonomy_term_revision", * revision_data_table = "taxonomy_term_field_revision", + * show_revision_ui = TRUE, * translatable = TRUE, * entity_keys = { * "id" = "tid", @@ -63,6 +69,10 @@ * "delete-form" = "/taxonomy/term/{taxonomy_term}/delete", * "edit-form" = "/taxonomy/term/{taxonomy_term}/edit", * "create" = "/taxonomy/term", + * "revision" = "/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term_revision}/view", + * "revision-delete-form" = "/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term_revision}/delete", + * "revision-revert-form" = "/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term_revision}/revert", + * "version-history" = "/taxonomy/term/{taxonomy_term}/revisions", * }, * permission_granularity = "bundle", * constraints = { diff --git a/core/modules/taxonomy/src/TaxonomyPermissions.php b/core/modules/taxonomy/src/TaxonomyPermissions.php index 054b638a7a5eae8c64e228889f9d116d15ca9464..3d3dea775c2fc116b6cf6cb58fd30b82da822bb8 100644 --- a/core/modules/taxonomy/src/TaxonomyPermissions.php +++ b/core/modules/taxonomy/src/TaxonomyPermissions.php @@ -69,6 +69,15 @@ protected function buildPermissions(VocabularyInterface $vocabulary) { "create terms in $id" => ['title' => $this->t('%vocabulary: Create terms', $args)], "delete terms in $id" => ['title' => $this->t('%vocabulary: Delete terms', $args)], "edit terms in $id" => ['title' => $this->t('%vocabulary: Edit terms', $args)], + "view terms revisions in $id" => ['title' => $this->t('%vocabulary: View terms revisions', $args)], + "revert terms revisions in $id" => [ + 'title' => $this->t('%vocabulary: Revert terms revisions', $args), + 'description' => $this->t('To revert a revision you also need permission to edit the taxonomy term.'), + ], + "delete terms revisions in $id" => [ + 'title' => $this->t('%vocabulary: Delete terms revisions', $args), + 'description' => $this->t('To delete a revision you also need permission to delete the taxonomy term.'), + ], ]; } diff --git a/core/modules/taxonomy/src/TermAccessControlHandler.php b/core/modules/taxonomy/src/TermAccessControlHandler.php index b25dca4627b9fb4d9a54733f617b2ba2c1763fee..007ed70313650830e8489a05e0da69301b5cba6c 100644 --- a/core/modules/taxonomy/src/TermAccessControlHandler.php +++ b/core/modules/taxonomy/src/TermAccessControlHandler.php @@ -46,6 +46,30 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter return AccessResult::neutral()->setReason("The following permissions are required: 'delete terms in {$entity->bundle()}' OR 'administer taxonomy'."); + case 'view revision': + if ($account->hasPermission("view terms revisions in {$entity->bundle()}")) { + return AccessResult::allowed()->cachePerPermissions(); + } + return AccessResult::neutral()->setReason("The following permissions are required: 'view revisions in {$entity->bundle()}' OR 'view all taxonomy revisions'."); + + case 'revert revision': + if ($entity->isDefaultRevision() || $entity->isLatestRevision()) { + return AccessResult::forbidden()->setReason("Revert or delete revision is not allowed on latest or default revision."); + } + elseif (($account->hasPermission("revert terms revisions in {$entity->bundle()}") && $account->hasPermission("edit terms in {$entity->bundle()}")) || $account->hasPermission("revert all taxonomy revisions")) { + return AccessResult::allowed()->cachePerPermissions(); + } + return AccessResult::neutral()->setReason("The following permissions are required: 'revert terms revisions in {$entity->bundle()}' OR 'revert all taxonomy revisions'."); + + case 'delete revision': + if ($entity->isDefaultRevision() || $entity->isLatestRevision()) { + return AccessResult::forbidden()->setReason("Revert or delete revision is not allowed on latest or default revision."); + } + elseif (($account->hasPermission("delete terms revisions in {$entity->bundle()}") && $account->hasPermission("delete terms in {$entity->bundle()}")) || $account->hasPermission("delete all taxonomy revisions")) { + return AccessResult::allowed()->cachePerPermissions(); + } + return AccessResult::neutral()->setReason("The following permissions are required: 'delete terms revisions in {$entity->bundle()}' OR 'delete all taxonomy revisions'."); + default: // No opinion. return AccessResult::neutral()->cachePerPermissions(); diff --git a/core/modules/taxonomy/taxonomy.install b/core/modules/taxonomy/taxonomy.install index b03fda2ac46eb00c4af01e9cc957367c5b046943..ae703e9a3fe2ce53e1f3793516f8d99c19091364 100644 --- a/core/modules/taxonomy/taxonomy.install +++ b/core/modules/taxonomy/taxonomy.install @@ -1,5 +1,11 @@ getEntityType('taxonomy_term'); + $routeProviders = $definition->get('route_provider'); + $routeProviders['revision'] = RevisionHtmlRouteProvider::class; + $definition + ->setFormClass('revision-delete', RevisionDeleteForm::class) + ->setFormClass('revision-revert', RevisionRevertForm::class) + ->set('route_provider', $routeProviders) + ->setLinkTemplate('revision-delete-form', '/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term}/delete') + ->setLinkTemplate('revision-revert-form', '/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term}/revert') + ->setLinkTemplate('version-history', '/taxonomy/term/{taxonomy_term}/revisions'); + $entityDefinitionUpdateManager->updateEntityType($definition); + return \t('Added revision routes to Taxonomy Term entity type.'); +} diff --git a/core/modules/taxonomy/taxonomy.permissions.yml b/core/modules/taxonomy/taxonomy.permissions.yml index bb71e93c124f4a004e7851971dffa5a09832d3f2..81874df3fc53d5751ba704c04e77349ad1b0b517 100644 --- a/core/modules/taxonomy/taxonomy.permissions.yml +++ b/core/modules/taxonomy/taxonomy.permissions.yml @@ -5,5 +5,11 @@ access taxonomy overview: title: 'Access the taxonomy vocabulary overview page' description: 'Get an overview of all taxonomy vocabularies.' +revert all taxonomy revisions: + title: 'Revert all terms revisions' + +delete all taxonomy revisions: + title: 'Delete all terms revisions' + permission_callbacks: - Drupal\taxonomy\TaxonomyPermissions::permissions diff --git a/core/modules/taxonomy/tests/src/Functional/TaxonomyRevisionDeleteTest.php b/core/modules/taxonomy/tests/src/Functional/TaxonomyRevisionDeleteTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ef490f73f585d1e90e63096f5a7df0bf067cd9a1 --- /dev/null +++ b/core/modules/taxonomy/tests/src/Functional/TaxonomyRevisionDeleteTest.php @@ -0,0 +1,102 @@ +vocabulary = $this->createVocabulary('test'); + } + + /** + * Tests revision delete. + */ + public function testDeleteForm(): void { + $entity = Term::create([ + 'vid' => $this->vocabulary->id(), + 'name' => 'Test taxonomy term', + ]); + + $entity->setRevisionCreationTime((new \DateTimeImmutable('11 January 2009 4pm'))->getTimestamp()) + ->setRevisionTranslationAffected(TRUE); + $entity->setNewRevision(); + $entity->save(); + $revisionId = $entity->getRevisionId(); + + // Cannot delete latest revision. + $this->drupalGet($entity->toUrl('revision-delete-form')); + $this->assertSession()->statusCodeEquals(403); + + // Create a new latest revision. + $entity + ->setRevisionCreationTime((new \DateTimeImmutable('11 January 2009 5pm'))->getTimestamp()) + ->setRevisionTranslationAffected(TRUE) + ->setNewRevision(); + $entity->save(); + + // Reload the entity. + $revision = \Drupal::entityTypeManager()->getStorage('taxonomy_term') + ->loadRevision($revisionId); + $this->drupalGet($revision->toUrl('revision-delete-form')); + $this->assertSession()->pageTextContains('Are you sure you want to delete the revision from Sun, 01/11/2009 - 16:00?'); + $this->assertSession()->buttonExists('Delete'); + $this->assertSession()->linkExists('Cancel'); + + $countRevisions = static function (): int { + return (int) \Drupal::entityTypeManager()->getStorage('taxonomy_term') + ->getQuery() + ->accessCheck(FALSE) + ->allRevisions() + ->count() + ->execute(); + }; + + $count = $countRevisions(); + $this->submitForm([], 'Delete'); + $this->assertEquals($count - 1, $countRevisions()); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->addressEquals(sprintf('taxonomy/term/%s/revisions', $entity->id())); + $this->assertSession()->pageTextContains(sprintf('Revision from Sun, 01/11/2009 - 16:00 of basic %s has been deleted.', $entity->label())); + $this->assertSession()->elementsCount('css', 'table tbody tr', 1); + } + +} diff --git a/core/modules/taxonomy/tests/src/Functional/TaxonomyRevisionRevertTest.php b/core/modules/taxonomy/tests/src/Functional/TaxonomyRevisionRevertTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4d81a41f935dca2604bce7447b28edc2add55e0d --- /dev/null +++ b/core/modules/taxonomy/tests/src/Functional/TaxonomyRevisionRevertTest.php @@ -0,0 +1,110 @@ +vocabulary = $this->createVocabulary('test'); + } + + + /** + * Tests revision revert. + */ + public function testRevertForm(): void { + $entity = Term::create([ + 'vid' => $this->vocabulary->id(), + 'name' => 'Test taxonomy term', + ]); + + $entity->setRevisionCreationTime((new \DateTimeImmutable('11 January 2009 4pm'))->getTimestamp()) + ->setRevisionTranslationAffected(TRUE); + $entity->setNewRevision(); + $entity->save(); + $revisionId = $entity->getRevisionId(); + + // Cannot revert latest revision. + $this->drupalGet($entity->toUrl('revision-revert-form')); + $this->assertSession()->statusCodeEquals(403); + + // Create a new latest revision. + $entity + ->setRevisionCreationTime((new \DateTimeImmutable('11 January 2009 5pm'))->getTimestamp()) + ->setRevisionTranslationAffected(TRUE) + ->setNewRevision(); + $entity->save(); + + // Reload the entity. + $revision = \Drupal::entityTypeManager()->getStorage('taxonomy_term') + ->loadRevision($revisionId); + $this->drupalGet($revision->toUrl('revision-revert-form')); + $this->assertSession()->pageTextContains('Are you sure you want to revert to the revision from Sun, 01/11/2009 - 16:00?'); + $this->assertSession()->buttonExists('Revert'); + $this->assertSession()->linkExists('Cancel'); + + $countRevisions = static function (): int { + return (int) \Drupal::entityTypeManager()->getStorage('taxonomy_term') + ->getQuery() + ->accessCheck(FALSE) + ->allRevisions() + ->count() + ->execute(); + }; + + $count = $countRevisions(); + $this->submitForm([], 'Revert'); + $this->assertEquals($count + 1, $countRevisions()); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->addressEquals(sprintf('taxonomy/term/%s/revisions', $entity->id())); + $this->assertSession()->pageTextContains(sprintf('term \'%s\' has been reverted to the revision from Sun, 01/11/2009 - 16:00.', $entity->label())); + $this->assertSession()->elementsCount('css', 'table tbody tr', 3); + } + +} diff --git a/core/modules/taxonomy/tests/src/Functional/TaxonomyRevisionVersionHistoryTest.php b/core/modules/taxonomy/tests/src/Functional/TaxonomyRevisionVersionHistoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..735d5eddcde8112ff728bedfe0141609e6bb5254 --- /dev/null +++ b/core/modules/taxonomy/tests/src/Functional/TaxonomyRevisionVersionHistoryTest.php @@ -0,0 +1,111 @@ +vocabulary = $this->createVocabulary('test'); + } + + /** + * Tests version history page. + */ + public function testVersionHistory(): void { + $entity = Term::create([ + 'vid' => $this->vocabulary->id(), + 'name' => 'Test taxonomy term', + ]); + + $entity + ->setInfo('first revision') + ->setRevisionCreationTime((new \DateTimeImmutable('1st June 2020 7am'))->getTimestamp()) + ->setRevisionLogMessage('first revision log') + ->setRevisionUser($this->drupalCreateUser(name: 'first author')) + ->setNewRevision(); + $entity->save(); + + $entity + ->setInfo('second revision') + ->setRevisionCreationTime((new \DateTimeImmutable('2nd June 2020 8am'))->getTimestamp()) + ->setRevisionLogMessage('second revision log') + ->setRevisionUser($this->drupalCreateUser(name: 'second author')) + ->setNewRevision(); + $entity->save(); + + $entity + ->setInfo('third revision') + ->setRevisionCreationTime((new \DateTimeImmutable('3rd June 2020 9am'))->getTimestamp()) + ->setRevisionLogMessage('third revision log') + ->setRevisionUser($this->drupalCreateUser(name: 'third author')) + ->setNewRevision(); + $entity->save(); + + $this->drupalGet($entity->toUrl('version-history')); + $this->assertSession()->elementsCount('css', 'table tbody tr', 3); + + // Order is newest to oldest revision by creation order. + $row1 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(1)'); + // Latest revision does not have revert or delete revision operation. + $this->assertSession()->elementNotExists('named', ['link', 'Revert'], $row1); + $this->assertSession()->elementNotExists('named', ['link', 'Delete'], $row1); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', 'Current revision'); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', 'third revision log'); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', '06/03/2020 - 09:00 by third author'); + + $row2 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(2)'); + $this->assertSession()->elementExists('named', ['link', 'Revert'], $row2); + $this->assertSession()->elementExists('named', ['link', 'Delete'], $row2); + $this->assertSession()->elementTextNotContains('css', 'table tbody tr:nth-child(2)', 'Current revision'); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(2)', 'second revision log'); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(2)', '06/02/2020 - 08:00 by second author'); + + $row3 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(3)'); + $this->assertSession()->elementExists('named', ['link', 'Revert'], $row3); + $this->assertSession()->elementExists('named', ['link', 'Delete'], $row3); + $this->assertSession()->elementTextNotContains('css', 'table tbody tr:nth-child(2)', 'Current revision'); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(3)', 'first revision log'); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(3)', '06/01/2020 - 07:00 by first author'); + } + +}