diff --git a/core/modules/content_moderation/src/Access/LatestRevisionCheck.php b/core/modules/content_moderation/src/Access/LatestRevisionCheck.php index 528d195..61ccff2 100644 --- a/core/modules/content_moderation/src/Access/LatestRevisionCheck.php +++ b/core/modules/content_moderation/src/Access/LatestRevisionCheck.php @@ -7,6 +7,8 @@ use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\content_moderation\ModerationInformationInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\user\EntityOwnerInterface; use Symfony\Component\Routing\Route; /** @@ -41,18 +43,25 @@ public function __construct(ModerationInformationInterface $moderation_informati * The route to check against. * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The parametrized route. + * @param \Drupal\Core\Session\AccountInterface $account + * The current user account. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. * * @see \Drupal\Core\Entity\EntityAccessCheck */ - public function access(Route $route, RouteMatchInterface $route_match) { + public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) { // This tab should not show up unless there's a reason to show it. $entity = $this->loadEntity($route, $route_match); - return $this->moderationInfo->hasForwardRevision($entity) - ? AccessResult::allowed()->addCacheableDependency($entity) - : AccessResult::forbidden()->addCacheableDependency($entity); + if ($this->moderationInfo->hasForwardRevision($entity)) { + return AccessResult::allowedIfHasPermissions($account, ['view latest version', 'view any unpublished content']) + ->orIf(AccessResult::allowedIfHasPermissions($account, ['view latest version', 'view own unpublished content']) + ->andif(AccessResult::allowedIf($entity instanceof EntityOwnerInterface && $entity->getOwnerId() == $account->id())) + )->addCacheableDependency($entity); + } + + return AccessResult::forbidden()->addCacheableDependency($entity); } /** diff --git a/core/modules/content_moderation/src/Routing/EntityModerationRouteProvider.php b/core/modules/content_moderation/src/Routing/EntityModerationRouteProvider.php index 6e0b2b9..e96f374 100644 --- a/core/modules/content_moderation/src/Routing/EntityModerationRouteProvider.php +++ b/core/modules/content_moderation/src/Routing/EntityModerationRouteProvider.php @@ -81,7 +81,6 @@ protected function getLatestVersionRoute(EntityTypeInterface $entity_type) { // If the entity type is a node, unpublished content will be visible // if the user has the "view all unpublished content" permission. ->setRequirement('_entity_access', "{$entity_type_id}.view") - ->setRequirement('_permission', 'view latest version,view any unpublished content') ->setRequirement('_content_moderation_latest_version', 'TRUE') ->setOption('_content_moderation_entity_type', $entity_type_id) ->setOption('parameters', [ diff --git a/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php b/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php index 1f8838b..50e0ac7 100644 --- a/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php +++ b/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php @@ -3,12 +3,19 @@ namespace Drupal\Tests\content_moderation\Unit; use Drupal\block_content\Entity\BlockContent; +use Drupal\Component\DependencyInjection\Container; use Drupal\Core\Access\AccessResultAllowed; use Drupal\Core\Access\AccessResultForbidden; +use Drupal\Core\Access\AccessResultNeutral; +use Drupal\Core\Cache\Context\CacheContextsManager; use Drupal\Core\Routing\RouteMatch; +use Drupal\Core\Session\AccountInterface; use Drupal\node\Entity\Node; use Drupal\content_moderation\Access\LatestRevisionCheck; use Drupal\content_moderation\ModerationInformation; +use Drupal\user\EntityOwnerInterface; +use Prophecy\Argument; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Routing\Route; /** @@ -18,6 +25,20 @@ class LatestRevisionCheckTest extends \PHPUnit_Framework_TestCase { /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Initialize Drupal container since the cache context manager is needed. + $contexts_manager = $this->prophesize(CacheContextsManager::class); + $contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE); + $builder = new ContainerBuilder(); + $builder->set('cache_contexts_manager', $contexts_manager->reveal()); + \Drupal::setContainer($builder); + } + + /** * Test the access check of the LatestRevisionCheck service. * * @param string $entity_class @@ -26,19 +47,38 @@ class LatestRevisionCheckTest extends \PHPUnit_Framework_TestCase { * The machine name of the entity to mock. * @param bool $has_forward * Whether this entity should have a forward revision in the system. + * @param array $account_permissions + * An array of permissions the account has. + * @param bool $is_owner + * Indicates if the user should be the owner of the entity. * @param string $result_class * The AccessResult class that should result. One of AccessResultAllowed, * AccessResultForbidden, AccessResultNeutral. * * @dataProvider accessSituationProvider */ - public function testLatestAccessPermissions($entity_class, $entity_type, $has_forward, $result_class) { + public function testLatestAccessPermissions($entity_class, $entity_type, $has_forward, array $account_permissions, $is_owner, $result_class) { + + /** @var \Drupal\Core\Session\AccountInterface $account */ + $account = $this->prophesize(AccountInterface::class); + $possible_permissions = [ + 'view latest version', + 'view any unpublished content', + 'view own unpublished content', + ]; + foreach ($possible_permissions as $permission) { + $account->hasPermission($permission)->willReturn(in_array($permission, $account_permissions)); + } + $account->id()->willReturn(42); /** @var \Drupal\Core\Entity\EntityInterface $entity */ $entity = $this->prophesize($entity_class); $entity->getCacheContexts()->willReturn([]); $entity->getCacheTags()->willReturn([]); $entity->getCacheMaxAge()->willReturn(0); + if (is_subclass_of($entity_class, EntityOwnerInterface::class)) { + $entity->getOwnerId()->willReturn($is_owner ? 42 : 3); + } /** @var \Drupal\content_moderation\ModerationInformation $mod_info */ $mod_info = $this->prophesize(ModerationInformation::class); @@ -54,7 +94,7 @@ public function testLatestAccessPermissions($entity_class, $entity_type, $has_fo $lrc = new LatestRevisionCheck($mod_info->reveal()); /** @var \Drupal\Core\Access\AccessResult $result */ - $result = $lrc->access($route->reveal(), $route_match->reveal()); + $result = $lrc->access($route->reveal(), $route_match->reveal(), $account->reveal()); $this->assertInstanceOf($result_class, $result); @@ -65,10 +105,24 @@ public function testLatestAccessPermissions($entity_class, $entity_type, $has_fo */ public function accessSituationProvider() { return [ - [Node::class, 'node', TRUE, AccessResultAllowed::class], - [Node::class, 'node', FALSE, AccessResultForbidden::class], - [BlockContent::class, 'block_content', TRUE, AccessResultAllowed::class], - [BlockContent::class, 'block_content', FALSE, AccessResultForbidden::class], + // Node with global permissions and latest version. + [Node::class, 'node', TRUE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultAllowed::class], + // Node with global permissions and no latest version. + [Node::class, 'node', FALSE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultForbidden::class], + // Node with own content permissions and latest version. + [Node::class, 'node', TRUE, ['view latest version', 'view own unpublished content'], TRUE, AccessResultAllowed::class], + // Node with own content permissions and no latest version. + [Node::class, 'node', TRUE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultNeutral::class], + // Node with own content permissions and latest version, but no perms to + // view latest version. + [Node::class, 'node', TRUE, ['view own unpublished content'], TRUE, AccessResultNeutral::class], + // Node with own content permissions and no latest version, but no perms + // to view latest version. + [Node::class, 'node', TRUE, ['view own unpublished content'], FALSE, AccessResultNeutral::class], + [BlockContent::class, 'block_content', TRUE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultAllowed::class], + [BlockContent::class, 'block_content', FALSE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultForbidden::class], + [BlockContent::class, 'block_content', TRUE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultNeutral::class], + [BlockContent::class, 'block_content', FALSE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultForbidden::class], ]; }