diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php index 7244bdfeb4d8352b1f49b73d268bc027ea974ebd..7131083bb5ac6e4333b315d402c75dfdb2afbd30 100644 --- a/core/modules/node/src/Controller/NodeController.php +++ b/core/modules/node/src/Controller/NodeController.php @@ -127,7 +127,7 @@ public function add(NodeTypeInterface $node_type) { public function revisionShow($node_revision) { $node = $this->entityManager()->getStorage('node')->loadRevision($node_revision); $node = $this->entityManager()->getTranslationFromContext($node); - $node_view_controller = new NodeViewController($this->entityManager, $this->renderer); + $node_view_controller = new NodeViewController($this->entityManager, $this->renderer, $this->currentUser()); $page = $node_view_controller->view($node); unset($page['nodes'][$node->id()]['#cache']); return $page; diff --git a/core/modules/node/src/Controller/NodeViewController.php b/core/modules/node/src/Controller/NodeViewController.php index f80e65d27e2f3a9c22083c42513a4f814d30fd6d..fce50e144ab6e78473a2c70729a130141bf446d4 100644 --- a/core/modules/node/src/Controller/NodeViewController.php +++ b/core/modules/node/src/Controller/NodeViewController.php @@ -4,12 +4,50 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\Controller\EntityViewController; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Session\AccountInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines a controller to render a single node. */ class NodeViewController extends EntityViewController { + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * Creates an NodeViewController object. + * + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user. For backwards compatibility this is optional, however + * this will be removed before Drupal 9.0.0. + */ + public function __construct(EntityManagerInterface $entity_manager, RendererInterface $renderer, AccountInterface $current_user = NULL) { + parent::__construct($entity_manager, $renderer); + $this->currentUser = $current_user ?: \Drupal::currentUser(); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager'), + $container->get('renderer'), + $container->get('current_user') + ); + } + /** * {@inheritdoc} */ @@ -17,27 +55,44 @@ public function view(EntityInterface $node, $view_mode = 'full', $langcode = NUL $build = parent::view($node, $view_mode, $langcode); foreach ($node->uriRelationships() as $rel) { - // Set the node path as the canonical URL to prevent duplicate content. - $build['#attached']['html_head_link'][] = array( - array( - 'rel' => $rel, - 'href' => $node->url($rel), - ), - TRUE, - ); + $url = $node->toUrl($rel); + // Add link relationships if the user is authenticated or if the anonymous + // user has access. Access checking must be done for anonymous users to + // avoid traffic to inaccessible pages from web crawlers. For + // authenticated users, showing the links in HTML head does not impact + // user experience or security, since the routes are access checked when + // visited and only visible via view source. This prevents doing + // potentially expensive and hard to cache access checks on every request. + // This means that the page will vary by user.permissions. We also rely on + // the access checking fallback to ensure the correct cacheability + // metadata if we have to check access. + if ($this->currentUser->isAuthenticated() || $url->access($this->currentUser)) { + // Set the node path as the canonical URL to prevent duplicate content. + $build['#attached']['html_head_link'][] = array( + array( + 'rel' => $rel, + 'href' => $url->toString(), + ), + TRUE, + ); + } if ($rel == 'canonical') { // Set the non-aliased canonical path as a default shortlink. $build['#attached']['html_head_link'][] = array( array( 'rel' => 'shortlink', - 'href' => $node->url($rel, array('alias' => TRUE)), + 'href' => $url->setOption('alias', TRUE)->toString(), ), TRUE, ); } } + // Given this varies by $this->currentUser->isAuthenticated(), add a cache + // context based on the anonymous role. + $build['#cache']['contexts'][] = 'user.roles:anonymous'; + return $build; } diff --git a/core/modules/node/src/Tests/NodeCacheTagsTest.php b/core/modules/node/src/Tests/NodeCacheTagsTest.php index 990f00b24f6a3a5733b924a43c79edd9c2df8934..66932cce084bb60037eea0a93cd980b8b7efb61b 100644 --- a/core/modules/node/src/Tests/NodeCacheTagsTest.php +++ b/core/modules/node/src/Tests/NodeCacheTagsTest.php @@ -38,6 +38,16 @@ protected function createEntity() { return $node; } + /** + * {@inheritdoc} + */ + protected function getDefaultCacheContexts() { + $defaults = parent::getDefaultCacheContexts(); + // @see \Drupal\node\Controller\NodeViewController::view() + $defaults[] = 'user.roles:anonymous'; + return $defaults; + } + /** * {@inheritdoc} */ diff --git a/core/modules/node/src/Tests/NodeViewTest.php b/core/modules/node/src/Tests/NodeViewTest.php index 5b2200c10d35775a84c98d6bf04e1d3ac3d0d022..fae056c6fdf2dc14936d8483a68fa4b478915161 100644 --- a/core/modules/node/src/Tests/NodeViewTest.php +++ b/core/modules/node/src/Tests/NodeViewTest.php @@ -18,14 +18,49 @@ public function testHtmlHeadLinks() { $this->drupalGet($node->urlInfo()); + $result = $this->xpath('//link[@rel = "canonical"]'); + $this->assertEqual($result[0]['href'], $node->url()); + + // Link relations are checked for access for anonymous users. + $result = $this->xpath('//link[@rel = "version-history"]'); + $this->assertFalse($result, 'Version history not present for anonymous users without access.'); + + $result = $this->xpath('//link[@rel = "edit-form"]'); + $this->assertFalse($result, 'Edit form not present for anonymous users without access.'); + + $this->drupalLogin($this->createUser(['access content'])); + $this->drupalGet($node->urlInfo()); + + $result = $this->xpath('//link[@rel = "canonical"]'); + $this->assertEqual($result[0]['href'], $node->url()); + + // Link relations are present regardless of access for authenticated users. $result = $this->xpath('//link[@rel = "version-history"]'); $this->assertEqual($result[0]['href'], $node->url('version-history')); $result = $this->xpath('//link[@rel = "edit-form"]'); $this->assertEqual($result[0]['href'], $node->url('edit-form')); + // Give anonymous users access to edit the node. Do this through the UI to + // ensure caches are handled properly. + $this->drupalLogin($this->rootUser); + $edit = [ + 'anonymous[edit own ' . $node->bundle() . ' content]' => TRUE + ]; + $this->drupalPostForm('admin/people/permissions', $edit, 'Save permissions'); + $this->drupalLogout(); + + // Anonymous user's should now see the edit-form link but not the + // version-history link. + $this->drupalGet($node->urlInfo()); $result = $this->xpath('//link[@rel = "canonical"]'); $this->assertEqual($result[0]['href'], $node->url()); + + $result = $this->xpath('//link[@rel = "version-history"]'); + $this->assertFalse($result, 'Version history not present for anonymous users without access.'); + + $result = $this->xpath('//link[@rel = "edit-form"]'); + $this->assertEqual($result[0]['href'], $node->url('edit-form')); } /** diff --git a/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php b/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php index 363fa9297bbd45347f9e9af02e5179c924346d4d..ad4f59b9faf43fa172fc12ba5cd23b33cecb437c 100644 --- a/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php +++ b/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php @@ -27,7 +27,7 @@ public function testEntityUri() { $view_mode = $this->selectViewMode($entity_type); // The default cache contexts for rendered entities. - $entity_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; + $entity_cache_contexts = $this->getDefaultCacheContexts(); // Generate the standardized entity cache tags. $cache_tag = $this->entity->getCacheTags(); @@ -141,4 +141,14 @@ public function testEntityUri() { $this->assertResponse(404); } + /** + * Gets the default cache contexts for rendered entities. + * + * @return array + * The default cache contexts for rendered entities. + */ + protected function getDefaultCacheContexts() { + return ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; + } + }