diff --git a/core/modules/node/src/NodeAccessControlHandler.php b/core/modules/node/src/NodeAccessControlHandler.php index 963ab53ded4129547c9480f9d29937de2ff7b21a..11561df302fe734fc3e18ab7f3818d6ebc3fce0c 100644 --- a/core/modules/node/src/NodeAccessControlHandler.php +++ b/core/modules/node/src/NodeAccessControlHandler.php @@ -131,12 +131,11 @@ protected function checkAccess(EntityInterface $node, $operation, AccountInterfa assert($node instanceof NodeInterface); $cacheability = new CacheableMetadata(); + $view_access_result = NULL; + /** @var \Drupal\node\NodeInterface $node */ if ($operation === 'view') { - $result = $this->checkViewAccess($node, $account, $cacheability); - if ($result !== NULL) { - return $result; - } + $view_access_result = $this->checkViewAccess($node, $account, $cacheability); } [$revision_permission_operation, $entity_operation] = static::REVISION_OPERATION_MAP[$operation] ?? [ @@ -185,6 +184,9 @@ protected function checkAccess(EntityInterface $node, $operation, AccountInterfa $access_result = $this->grantStorage->access($node, $operation, $account); if ($access_result instanceof RefinableCacheableDependencyInterface) { $access_result->addCacheableDependency($cacheability); + if ($view_access_result) { + $access_result->addCacheableDependency($view_access_result); + } } return $access_result; } @@ -216,14 +218,31 @@ protected function checkViewAccess(NodeInterface $node, AccountInterface $accoun return NULL; } + // Due to the check below, it is not possible to rely only on account + // permissions to determine whether the 'view own unpublished content' + // permission can be checked, instead we also need to check if the user has + // the authenticated role. Just in case anonymous and authenticated users + // are both granted the 'view own unpublished content' permission and also + // have otherwise identical permissions. $cacheability->addCacheContexts(['user.roles:authenticated']); + // The "view own unpublished content" permission must not be granted // to anonymous users for security reasons. if (!$account->isAuthenticated()) { return NULL; } + // When access is granted due to the 'view own unpublished content' + // permission and for no other reason, node grants are bypassed. However, + // to ensure the full set of cacheable metadata is available to variation + // cache, additionally add the node_grants cache context so that if the + // status or the owner of the node changes, cache redirects will continue to + // reflect the latest state without needing to be invalidated. $cacheability->addCacheContexts(['user']); + if ($this->moduleHandler->hasImplementations('node_grants')) { + $cacheability->addCacheContexts(['user.node_grants:view']); + } + if ($account->id() != $node->getOwnerId()) { return NULL; } diff --git a/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarningTest.php b/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarningTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8946ca75fefeee4734ce41eafc516ac136b8a5d7 --- /dev/null +++ b/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarningTest.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\node\Functional; + +/** + * Tests the node access grants cache context service. + * + * @group node + * @group Cache + */ +class NodeAccessCacheRedirectWarningTest extends NodeTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['block', 'node_access_test_empty']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + node_access_rebuild(); + } + + /** + * Quick demonstration of the differences in cache contexts. + * + * The intent here was to visit the nodes to view the error but for whatever + * reason I can't seem to trigger the redirect warning this way. Needs work. + */ + public function testNodeAccessCacheRedirectWarning(): void { + $this->drupalPlaceBlock('local_tasks_block'); + + $this->assertTrue(\Drupal::moduleHandler()->hasImplementations('node_grants')); + + $author = $this->drupalCreateUser([ + 'create page content', + 'edit any page content', + 'view own unpublished content', + ]); + $this->drupalLogin($author); + + $node = $this->drupalCreateNode(['uid' => $author->id(), 'status' => 0]); + + $this->drupalGet($node->toUrl()); + + $node->setPublished(); + $node->save(); + + $this->drupalGet($node->toUrl()); + + $node->setUnpublished(); + $node->save(); + + $this->drupalGet($node->toUrl()); + + $node->setPublished(); + $node->save(); + + $this->drupalGet($node->toUrl()); + } + +}