diff --git a/core/modules/content_moderation/content_moderation.permissions.yml b/core/modules/content_moderation/content_moderation.permissions.yml index 18790fb..458b49f 100644 --- a/core/modules/content_moderation/content_moderation.permissions.yml +++ b/core/modules/content_moderation/content_moderation.permissions.yml @@ -12,3 +12,4 @@ view latest version: permission_callbacks: - \Drupal\content_moderation\Permissions::transitionPermissions + - \Drupal\content_moderation\Permissions::perBundleUnpublishedPermissions diff --git a/core/modules/content_moderation/src/Access/LatestRevisionCheck.php b/core/modules/content_moderation/src/Access/LatestRevisionCheck.php index db0cde4..cbf5753 100644 --- a/core/modules/content_moderation/src/Access/LatestRevisionCheck.php +++ b/core/modules/content_moderation/src/Access/LatestRevisionCheck.php @@ -57,6 +57,12 @@ public function access(Route $route, RouteMatchInterface $route_match, AccountIn if ($this->moderationInfo->hasForwardRevision($entity)) { // Check the global permissions first. $access_result = AccessResult::allowedIfHasPermissions($account, ['view latest version', 'view any unpublished content']); + + // Check per bundle unpublished access. + if (!$access_result->isAllowed()) { + $per_bundle_access = AccessResult::allowedIfHasPermissions($account, ['view latest version', "view any unpublished {$entity->getEntityTypeId()}:{$entity->bundle()} content"]); + $access_result = $access_result->orIf($per_bundle_access); + } if (!$access_result->isAllowed()) { // Check entity owner access. $owner_access = AccessResult::allowedIfHasPermissions($account, ['view latest version', 'view own unpublished content']); diff --git a/core/modules/content_moderation/src/Permissions.php b/core/modules/content_moderation/src/Permissions.php index efa6849..cc60cde 100644 --- a/core/modules/content_moderation/src/Permissions.php +++ b/core/modules/content_moderation/src/Permissions.php @@ -2,17 +2,69 @@ namespace Drupal\content_moderation; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\workflows\Entity\Workflow; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines a class for dynamic permissions based on transitions. */ -class Permissions { +class Permissions implements ContainerInjectionInterface { use StringTranslationTrait; /** + * The bundle information service. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $bundleInfo; + + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The moderation information service. + * + * @var \Drupal\content_moderation\ModerationInformationInterface + */ + protected $moderationInfo; + + /** + * Permissions constructor. + * + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info + * The entity bundle information service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager service. + * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information + * The moderation information service. + */ + public function __construct(EntityTypeBundleInfoInterface $bundle_info, EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_information) { + $this->bundleInfo = $bundle_info; + $this->entityTypeManager = $entity_type_manager; + $this->moderationInfo = $moderation_information; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.bundle.info'), + $container->get('entity_type.manager'), + $container->get('content_moderation.moderation_information') + ); + } + + /** * Returns an array of transition permissions. * * @return array @@ -35,4 +87,28 @@ public function transitionPermissions() { return $permissions; } + /** + * Returns an array of per-entity and bundle unpublished permissions. + * + * @return array + * The per-entity-bundle permissions for viewing unpublished content. + */ + public function perBundleUnpublishedPermissions() { + $permissions = []; + foreach ($this->entityTypeManager->getDefinitions() as $entity_type) { + if ($this->moderationInfo->canModerateEntitiesOfEntityType($entity_type)) { + foreach ($this->bundleInfo->getBundleInfo($entity_type->id()) as $bundle_id => $bundle_information) { + if ($this->moderationInfo->shouldModerateEntitiesOfBundle($entity_type, $bundle_id)) + $permissions["view any unpublished {$entity_type->id()}:{$bundle_id} content"] = [ + 'title' => $this->t('%entity_type: View any unpublished %bundle content', [ + '%entity_type' => $entity_type->getLabel(), + '%bundle' => $bundle_information['label'], + ]), + ]; + } + } + } + return $permissions; + } + } diff --git a/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php b/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php index 2d33ee8..76e2714 100644 --- a/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php +++ b/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php @@ -45,6 +45,8 @@ protected function setUp() { * The class of the entity to mock. * @param string $entity_type * The machine name of the entity to mock. + * @param string $bundle + * The bundle ID. * @param bool $has_forward * Whether this entity should have a forward revision in the system. * @param array $account_permissions @@ -57,7 +59,7 @@ protected function setUp() { * * @dataProvider accessSituationProvider */ - public function testLatestAccessPermissions($entity_class, $entity_type, $has_forward, array $account_permissions, $is_owner, $result_class) { + public function testLatestAccessPermissions($entity_class, $entity_type, $bundle, $has_forward, array $account_permissions, $is_owner, $result_class) { /** @var \Drupal\Core\Session\AccountInterface $account */ $account = $this->prophesize(AccountInterface::class); @@ -65,6 +67,7 @@ public function testLatestAccessPermissions($entity_class, $entity_type, $has_fo 'view latest version', 'view any unpublished content', 'view own unpublished content', + "view any unpublished $entity_type:$bundle content", ]; foreach ($possible_permissions as $permission) { $account->hasPermission($permission)->willReturn(in_array($permission, $account_permissions)); @@ -76,6 +79,8 @@ public function testLatestAccessPermissions($entity_class, $entity_type, $has_fo $entity->getCacheContexts()->willReturn([]); $entity->getCacheTags()->willReturn([]); $entity->getCacheMaxAge()->willReturn(0); + $entity->getEntityTypeId()->willReturn($entity_type); + $entity->bundle()->willReturn($bundle); if (is_subclass_of($entity_class, EntityOwnerInterface::class)) { $entity->getOwnerId()->willReturn($is_owner ? 42 : 3); } @@ -106,27 +111,31 @@ public function testLatestAccessPermissions($entity_class, $entity_type, $has_fo public function accessSituationProvider() { return [ // Node with global permissions and latest version. - [Node::class, 'node', TRUE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultAllowed::class], + [Node::class, 'node', 'article', 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::class, 'node', 'article', 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::class, 'node', 'article', TRUE, ['view latest version', 'view own unpublished content'], TRUE, AccessResultAllowed::class], // Node with own content permissions and no latest version. - [Node::class, 'node', FALSE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultForbidden::class], + [Node::class, 'node', 'article', FALSE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultForbidden::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::class, 'node', 'article', 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], + [Node::class, 'node', 'article', TRUE, ['view own unpublished content'], FALSE, AccessResultNeutral::class], + // Node with bundle-specific permissions. + [Node::class, 'node', 'article', TRUE, ['view latest version', 'view any unpublished node:article content'], FALSE, AccessResultAllowed::class], + // Node with bundle-specific permissions, but no permission to view latest version. + [Node::class, 'node', 'article', TRUE, ['view any unpublished node:article content'], FALSE, AccessResultNeutral::class], // Block with forward revision, and permissions to view any. - [BlockContent::class, 'block_content', TRUE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultAllowed::class], + [BlockContent::class, 'block_content', 'basic', TRUE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultAllowed::class], // Block with no forward revision. - [BlockContent::class, 'block_content', FALSE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultForbidden::class], + [BlockContent::class, 'block_content', 'basic', FALSE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultForbidden::class], // Block with forward revision, but no permission to view any. - [BlockContent::class, 'block_content', TRUE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultNeutral::class], + [BlockContent::class, 'block_content', 'basic', TRUE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultNeutral::class], // Block with no forward revision. - [BlockContent::class, 'block_content', FALSE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultForbidden::class], + [BlockContent::class, 'block_content', 'basic', FALSE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultForbidden::class], ]; }