From f6da2d352a982072124a2dc7c323d1493f60df6f Mon Sep 17 00:00:00 2001 From: Daniel Wehner <daniel@tag1consulting.com> Date: Sun, 14 Feb 2016 20:14:05 +0100 Subject: [PATCH] foo --- src/Controller/RevisionControllerTrait.php | 209 ++++++++++++++++++ src/Controller/RevisionOverviewController.php | 140 ++++++++++++ src/Routing/RevisionRouteProvider.php | 33 +++ .../src/Entity/EnhancedEntity.php | 3 +- tests/src/Kernel/RevisionBasicUITest.php | 33 +++ 5 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 src/Controller/RevisionControllerTrait.php create mode 100644 src/Controller/RevisionOverviewController.php diff --git a/src/Controller/RevisionControllerTrait.php b/src/Controller/RevisionControllerTrait.php new file mode 100644 index 0000000..c77e793 --- /dev/null +++ b/src/Controller/RevisionControllerTrait.php @@ -0,0 +1,209 @@ +<?php + +/** + * @file + * Contains \Drupal\entity\Controller\RevisionControllerTrait. + */ + +namespace Drupal\entity\Controller; + +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Defines a trait for common revision UI functionality. + */ +trait RevisionControllerTrait { + + use StringTranslationTrait; + + /** + * @return \Drupal\Core\Entity\EntityTypeManagerInterface + */ + abstract protected function entityTypeManager(); + + /** + * @return \Drupal\Core\Language\LanguageManagerInterface + */ + public abstract function languageManager(); + + /** + * Determines if the user has permission to revert revisions. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to check revert access for. + * + * @return bool + * TRUE if the user has revert access. + */ + abstract protected function hasRevertRevisionAccess(EntityInterface $entity); + + /** + * Determines if the user has permission to delete revisions. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to check delete revision access for. + * + * @return bool + * TRUE if the user has delete revision access. + */ + abstract protected function hasDeleteRevisionAccess(EntityInterface $entity); + + /** + * Builds a link to revert an entity revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity_revision + * The entity to build a revert revision link for. + * + * @return array A link render array. + * A link render array. + * @internal param int $revision_id The revision ID of the revert link.* The revision ID of the revert link. + * + */ + abstract protected function buildRevertRevisionLink(EntityInterface $entity_revision); + + /** + * Builds a link to delete an entity revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity_revision + * The entity to build a delete revision link for. + * + * @return array A link render array. + * A link render array. + * @internal param int $revision_id The revision ID of the delete link.* The revision ID of the delete link. + * + */ + abstract protected function buildDeleteRevisionLink(EntityInterface $entity_revision); + + /** + * Returns a string providing details of the revision. + * + * E.g. Node describes its revisions using {date} by {username}. For the + * non-current revision, it also provides a link to view that revision. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $revision + * @param bool $is_current + * TRUE if the revision is the current revision. + * + * @return string + * Returns a string to provide the details of the revision. + */ + abstract protected function getRevisionDescription(ContentEntityInterface $revision, $is_current = FALSE); + + /** + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * + * @return array + */ + protected function revisionIds(ContentEntityInterface $entity) { + $entity_type = $entity->getEntityTypeId(); + $result = $this->entityTypeManager()->getStorage($entity_type)->getQuery() + ->allRevisions() + ->condition($entity_type->getKey('id'), $entity->id()) + ->sort($entity_type->getKey('revision'), 'DESC') + ->execute(); + return array_keys($result); + } + + /** + * Generates an overview table of older revisions of an entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * An entity object. + * + * @return array + * An array as expected by drupal_render(). + */ + public function revisionOverview(ContentEntityInterface $entity) { + $langcode = $this->languageManager() + ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT) + ->getId(); + /** @var \Drupal\content_entity_base\Entity\Storage\RevisionableStorageInterface $entity_storage */ + $entity_storage = $this->entityTypeManager() + ->getStorage($entity->getEntityTypeId()); + + $header = [$this->t('Revision'), $this->t('Operations')]; + $rows = []; + + $revision_ids = $this->revisionIds($entity); + // @todo Expand the entity storage to load multiple revisions. + $entity_revisions = array_combine($revision_ids, array_map(function($vid) use ($entity_storage) { + return $entity_storage->loadRevision($vid); + }, $revision_ids)); + + $latest_revision = TRUE; + + foreach ($entity_revisions as $revision) { + $row = []; + /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */ + if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode) + ->isRevisionTranslationAffected() + ) { + if ($latest_revision) { + $row[] = $this->getRevisionDescription($revision, TRUE); + $row[] = [ + 'data' => [ + '#prefix' => '<em>', + '#markup' => $this->t('Current revision'), + '#suffix' => '</em>', + ], + ]; + foreach ($row as &$current) { + $current['class'] = ['revision-current']; + } + $latest_revision = FALSE; + } + else { + $row[] = $this->getRevisionDescription($revision, FALSE); + $links = $this->getOperationLinks($revision); + + $row[] = [ + 'data' => [ + '#type' => 'operations', + '#links' => $links, + ], + ]; + } + } + + $rows[] = $row; + } + + $build[$entity->getEntityTypeId() . '_revisions_table'] = [ + '#theme' => 'table', + '#rows' => $rows, + '#header' => $header, + ]; + + // We have no clue about caching yet. + $build['#cache']['max-age'] = 0; + + return $build; + } + + /** + * Get the links of the operations for an entity revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity_revision + * The entity to build the revision links for. + * + * @return array + * The operation links. + */ + protected function getOperationLinks(EntityInterface $entity_revision) { + $links = []; + $revert_permission = $this->hasRevertRevisionAccess($entity_revision); + $delete_permission = $this->hasDeleteRevisionAccess($entity_revision); + if ($revert_permission) { + $links['revert'] = $this->buildRevertRevisionLink($entity_revision); + } + + if ($delete_permission) { + $links['delete'] = $this->buildDeleteRevisionLink($entity_revision); + } + return $links; + } + +} diff --git a/src/Controller/RevisionOverviewController.php b/src/Controller/RevisionOverviewController.php new file mode 100644 index 0000000..1285b7d --- /dev/null +++ b/src/Controller/RevisionOverviewController.php @@ -0,0 +1,140 @@ +<?php + +/** + * @file + * Contains \Drupal\entity\Controller\RevisionOverviewController. + */ + +namespace Drupal\entity\Controller; + +use Drupal\Component\Utility\Xss; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Datetime\DateFormatterInterface; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\entity\Revision\EntityRevisionLogInterface; +use Drupal\user\EntityOwnerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class RevisionOverviewController extends ControllerBase { + + use RevisionControllerTrait; + + /** + * The date formatter. + * + * @var \Drupal\Core\Datetime\DateFormatterInterface + */ + protected $dateFormatter; + + /** + * Creates a new RevisionOverviewController instance. + * + * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter + * The date formatter. + */ + public function __construct(DateFormatterInterface $date_formatter) { + $this->dateFormatter = $date_formatter; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('date.formatter')); + } + + /** + * {@inheritdoc} + */ + protected function hasDeleteRevisionAccess(EntityInterface $entity) { + return $this->currentUser()->hasPermission("delete all {$entity->id()} revisions"); + } + + /** + * {@inheritdoc} + */ + protected function buildRevertRevisionLink(EntityInterface $entity_revision) { + return [ + 'title' => t('Revert'), + 'url' => $entity_revision->toUrl('revision-revert'), + ]; + } + + /** + * {@inheritdoc} + */ + protected function buildDeleteRevisionLink(EntityInterface $entity_revision) { + return [ + 'title' => t('Delete'), + 'url' => $entity_revision->toUrl('revision-delete'), + ]; + } + + /** + * {@inheritdoc} + */ + protected function getRevisionDescription(ContentEntityInterface $revision, $is_current = FALSE) { + /** @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\user\EntityOwnerInterface|\Drupal\entity\Revision\EntityRevisionLogInterface $revision */ + + if ($revision instanceof EntityOwnerInterface) { + $username = [ + '#theme' => 'username', + '#account' => $revision->getOwner(), + ]; + } + else { + $username = ''; + } + + if ($revision instanceof EntityRevisionLogInterface) { + // Use revision link to link to revisions that are not active. + $date = $this->dateFormatter->format($revision->getRevisionCreationTime(), 'short'); + if (!$is_current) { + $link = $revision->toLink($date, 'revision'); + } + else { + $link = $revision->toLink($date); + } + } + else { + $link = $revision->toLink($revision->label(), 'revision'); + } + + $markup = ''; + if ($revision instanceof EntityRevisionLogInterface) { + $markup = $revision->getRevisionLogMessage(); + } + + if ($username) { + $template = '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}'; + } + else { + $template = '{% trans %} {{ date }} {% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}'; + } + + $column = [ + 'data' => [ + '#type' => 'inline_template', + '#template' => $template, + '#context' => [ + 'date' => $link, + 'username' => $username, + 'message' => ['#markup' => $markup, '#allowed_tags' => Xss::getHtmlTagList()], + ], + ], + ]; + return $column; + } + + /** + * {@inheritdoc} + */ + protected function hasRevertRevisionAccess(EntityInterface $entity) { + return AccessResult::allowedIfHasPermission($this->currentUser(), "revert all {$entity->getEntityTypeId()} revisions")->orIf( + AccessResult::allowedIfHasPermission($this->currentUser(), "revert {$entity->bundle()} {$entity->getEntityTypeId()} revisions") + ); + } + +} diff --git a/src/Routing/RevisionRouteProvider.php b/src/Routing/RevisionRouteProvider.php index d539c61..870ece4 100644 --- a/src/Routing/RevisionRouteProvider.php +++ b/src/Routing/RevisionRouteProvider.php @@ -30,6 +30,10 @@ class RevisionRouteProvider implements EntityRouteProviderInterface { $collection->add("entity.$entity_type_id.revision_revert_form", $view_route); } + if ($view_route = $this->getRevisionHistoryRoute($entity_type)) { + $collection->add("entity.$entity_type_id.version_history", $view_route); + } + return $collection; } @@ -97,4 +101,33 @@ class RevisionRouteProvider implements EntityRouteProviderInterface { } } + /** + * Gets the entity revision version history route. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return \Symfony\Component\Routing\Route|null + * The generated route, if available. + */ + protected function getRevisionHistoryRoute($entity_type) { + if ($entity_type->hasLinkTemplate('version-history')) { + $entity_type_id = $entity_type->id(); + $route = new Route($entity_type->getLinkTemplate('revision')); + $route->addDefaults([ + '_controller' => '\Drupal\entity\Controller\RevisionControllerTrait::revisionOverview', + '_title' => 'Revisions', + ]); + $route->addRequirements([ + '_entity_access_revision' => "$entity_type_id.view", + ]); + $route->setOption('parameters', [ + $entity_type->id() => [ + 'type' => 'entity:' . $entity_type->id(), + ], + ]); + return $route; + } + } + } diff --git a/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php b/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php index d49e0bd..53c7ae4 100644 --- a/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php +++ b/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php @@ -51,8 +51,9 @@ use Drupal\entity\Revision\RevisionableContentEntityBase; * "canonical" = "/entity_test_enhanced/{entity_test_enhanced}", * "revision" = "/entity_test_enhanced/{entity_test_enhanced}/revisions/{entity_test_enhanced_revision}/view", * "revision-revert-form" = "/entity_test_enhanced/{entity_test_enhanced}/revisions/{entity_test_enhanced_revision}/revert", + * "version-history" = "/entity_test_enhanced/{entity_test_enhanced}/revisions", * }, - * bundle_entity_type = "entity_test_enhanced_bundle" + * bundle_entity_type = "entity_test_enhanced_bundle", * ) */ class EnhancedEntity extends RevisionableContentEntityBase { diff --git a/tests/src/Kernel/RevisionBasicUITest.php b/tests/src/Kernel/RevisionBasicUITest.php index efb6f75..48ad0dd 100644 --- a/tests/src/Kernel/RevisionBasicUITest.php +++ b/tests/src/Kernel/RevisionBasicUITest.php @@ -43,6 +43,39 @@ class RevisionBasicUITest extends KernelTestBase { \Drupal::service('router.builder')->rebuild(); } + public function testRevisionHistory() { + $entity = EnhancedEntity::create([ + 'name' => 'rev 1', + ]); + $entity->save(); + + $revision = clone $entity; + $revision->name->value = 'rev 2'; + $revision->setNewRevision(TRUE); + $revision->isDefaultRevision(FALSE); + $revision->save(); + + $http_kernel = \Drupal::service('http_kernel'); + $request = Request::create($revision->url('revision')); + $response = $http_kernel->handle($request); + $this->assertEquals(403, $response->getStatusCode()); + + $role = Role::create(['id' => 'test_role']); + $role->grantPermission('view all entity_test_enhanced revisions'); + $role->grantPermission('administer entity_test_enhanced'); + $role->save(); + + $user = User::create([ + 'name' => 'Test user', + ]); + $user->addRole($role->id()); + \Drupal::service('account_switcher')->switchTo($user); + + $request = Request::create($revision->url('revision')); + $response = $http_kernel->handle($request); + $this->assertEquals(200, $response->getStatusCode()); + } + public function testRevisionView() { $entity = EnhancedEntity::create([ 'name' => 'rev 1', -- GitLab