Browse Source
* Adding 'related' link headers for entity references (cherry picked from commit 10ac4e444b68c4fa8db13a04289acd68276b786b) * Using event listener now so REST requests get the headers too * Updating @covers annotation on test * Coding standards * Adding authZ test * Caching properly nowpull/756/head
dannylamb
7 years ago
committed by
Jared Whiklo
5 changed files with 349 additions and 44 deletions
@ -0,0 +1,136 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\islandora\EventSubscriber; |
||||||
|
|
||||||
|
use Drupal\Core\Entity\EntityFieldManager; |
||||||
|
use Drupal\Core\Routing\RouteMatchInterface; |
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface; |
||||||
|
use Symfony\Component\HttpKernel\Event\FilterResponseEvent; |
||||||
|
use Symfony\Component\HttpKernel\KernelEvents; |
||||||
|
|
||||||
|
/** |
||||||
|
* Class NodeLinkHeaderSubscriber. |
||||||
|
* |
||||||
|
* @package Drupal\islandora\EventSubscriber |
||||||
|
*/ |
||||||
|
class NodeLinkHeaderSubscriber implements EventSubscriberInterface { |
||||||
|
|
||||||
|
/** |
||||||
|
* The entity field manager. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Entity\EntityFieldManager |
||||||
|
*/ |
||||||
|
protected $entityFieldManager; |
||||||
|
|
||||||
|
/** |
||||||
|
* The route match object. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Routing\RouteMatchInterface |
||||||
|
*/ |
||||||
|
protected $routeMatch; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructor. |
||||||
|
* |
||||||
|
* @param \Drupal\Core\Entity\EntityFieldManager $entity_field_manager |
||||||
|
* The entity field manager. |
||||||
|
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match |
||||||
|
* The route match object. |
||||||
|
*/ |
||||||
|
public function __construct( |
||||||
|
EntityFieldManager $entity_field_manager, |
||||||
|
RouteMatchInterface $route_match |
||||||
|
) { |
||||||
|
$this->entityFieldManager = $entity_field_manager; |
||||||
|
$this->routeMatch = $route_match; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function getSubscribedEvents() { |
||||||
|
// Run this early so the headers get cached. |
||||||
|
$events[KernelEvents::RESPONSE][] = ['onResponse', 129]; |
||||||
|
|
||||||
|
return $events; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds node-specific link headers to appropriate responses. |
||||||
|
* |
||||||
|
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event |
||||||
|
* Event containing the response. |
||||||
|
*/ |
||||||
|
public function onResponse(FilterResponseEvent $event) { |
||||||
|
$response = $event->getResponse(); |
||||||
|
|
||||||
|
// Exit early if the response is already cached. |
||||||
|
if ($response->headers->get('X-Drupal-Dynamic-Cache') == 'HIT') { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!$response->isOk()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Hack the node out of the route. |
||||||
|
$route_object = $this->routeMatch->getRouteObject(); |
||||||
|
if (!$route_object) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$methods = $route_object->getMethods(); |
||||||
|
$is_get = in_array('GET', $methods); |
||||||
|
$is_head = in_array('HEAD', $methods); |
||||||
|
if (!($is_get || $is_head)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$route_contexts = $route_object->getOption('parameters'); |
||||||
|
if (!$route_contexts) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (!isset($route_contexts['node'])) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$node = $this->routeMatch->getParameter('node'); |
||||||
|
|
||||||
|
if (!$node) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Use the node to add link headers for each entity reference. |
||||||
|
$bundle = $node->bundle(); |
||||||
|
|
||||||
|
// Get all fields for the entity. |
||||||
|
$fields = $this->entityFieldManager->getFieldDefinitions('node', $bundle); |
||||||
|
|
||||||
|
// Strip out everything but entity references that are not base fields. |
||||||
|
$entity_reference_fields = array_filter($fields, function ($field) { |
||||||
|
return $field->getFieldStorageDefinition()->isBaseField() == FALSE && $field->getType() == "entity_reference"; |
||||||
|
}); |
||||||
|
|
||||||
|
// Collect links for referenced entities. |
||||||
|
$links = []; |
||||||
|
foreach ($entity_reference_fields as $field_name => $field_definition) { |
||||||
|
foreach ($node->get($field_name)->referencedEntities() as $referencedEntity) { |
||||||
|
// Headers are subject to an access check. |
||||||
|
if ($referencedEntity->access('view')) { |
||||||
|
$entity_url = $referencedEntity->url('canonical', ['absolute' => TRUE]); |
||||||
|
$field_label = $field_definition->label(); |
||||||
|
$links[] = "<$entity_url>; rel=\"related\"; title=\"$field_label\""; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Exit early if there aren't any. |
||||||
|
if (empty($links)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Add the link headers to the response. |
||||||
|
$response->headers->set('Link', $links, FALSE); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,147 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\Tests\islandora\Functional; |
||||||
|
|
||||||
|
use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait; |
||||||
|
use Drupal\Tests\media_entity\Functional\MediaEntityFunctionalTestTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests the RelatedLinkHeader view alter. |
||||||
|
* |
||||||
|
* @group islandora |
||||||
|
*/ |
||||||
|
class RelatedLinkHeaderTest extends IslandoraFunctionalTestBase { |
||||||
|
|
||||||
|
use EntityReferenceTestTrait; |
||||||
|
use MediaEntityFunctionalTestTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* Node that has entity reference field. |
||||||
|
* |
||||||
|
* @var \Drupal\node\NodeInterface |
||||||
|
*/ |
||||||
|
protected $referencer; |
||||||
|
|
||||||
|
/** |
||||||
|
* Node that has entity reference field, but it's empty. |
||||||
|
* |
||||||
|
* @var \Drupal\node\NodeInterface |
||||||
|
*/ |
||||||
|
protected $referenced; |
||||||
|
|
||||||
|
/** |
||||||
|
* Media to be referenced (to check authZ). |
||||||
|
* |
||||||
|
* @var \Drupal\media\MediaInterface |
||||||
|
*/ |
||||||
|
protected $media; |
||||||
|
|
||||||
|
/** |
||||||
|
* Node of a bundle that does _not_ have an entity reference field. |
||||||
|
* |
||||||
|
* @var \Drupal\node\NodeInterface |
||||||
|
*/ |
||||||
|
protected $other; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function setUp() { |
||||||
|
parent::setUp(); |
||||||
|
|
||||||
|
// Create a test content type with an entity reference field. |
||||||
|
$test_type_with_reference = $this->container->get('entity_type.manager')->getStorage('node_type')->create([ |
||||||
|
'type' => 'test_type_with_reference', |
||||||
|
'label' => 'Test Type With Reference', |
||||||
|
]); |
||||||
|
$test_type_with_reference->save(); |
||||||
|
|
||||||
|
// Add two entity reference fields. |
||||||
|
// One for nodes and one for media. |
||||||
|
$this->createEntityReferenceField('node', 'test_type_with_reference', 'field_reference', 'Referenced Entity', 'node', 'default', [], 2); |
||||||
|
$this->createEntityReferenceField('node', 'test_type_with_reference', 'field_media', 'Media Entity', 'media', 'default', [], 2); |
||||||
|
|
||||||
|
$this->other = $this->container->get('entity_type.manager')->getStorage('node')->create([ |
||||||
|
'type' => 'test_type', |
||||||
|
'title' => 'Test object w/o entity reference field', |
||||||
|
]); |
||||||
|
$this->other->save(); |
||||||
|
|
||||||
|
$this->referenced = $this->container->get('entity_type.manager')->getStorage('node')->create([ |
||||||
|
'type' => 'test_type_with_reference', |
||||||
|
'title' => 'Referenced', |
||||||
|
]); |
||||||
|
$this->referenced->save(); |
||||||
|
|
||||||
|
$media_bundle = $this->drupalCreateMediaBundle(); |
||||||
|
$this->media = $this->container->get('entity_type.manager')->getStorage('media')->create([ |
||||||
|
'bundle' => $media_bundle->id(), |
||||||
|
'name' => 'Media', |
||||||
|
]); |
||||||
|
$this->media->save(); |
||||||
|
|
||||||
|
$this->referencer = $this->container->get('entity_type.manager')->getStorage('node')->create([ |
||||||
|
'type' => 'test_type_with_reference', |
||||||
|
'title' => 'Referencer', |
||||||
|
'field_reference' => [$this->referenced->id()], |
||||||
|
'field_media' => [$this->media->id()], |
||||||
|
]); |
||||||
|
$this->referencer->save(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @covers \Drupal\islandora\EventSubscriber\NodeLinkHeaderSubscriber::onResponse |
||||||
|
*/ |
||||||
|
public function testRelatedLinkHeader() { |
||||||
|
// Create a test user that can see media. |
||||||
|
$account = $this->drupalCreateUser([ |
||||||
|
'view media', |
||||||
|
]); |
||||||
|
$this->drupalLogin($account); |
||||||
|
|
||||||
|
// Visit the other, there should not be a header since it does not even |
||||||
|
// have the field. |
||||||
|
$this->drupalGet('node/' . $this->other->id()); |
||||||
|
$this->assertTrue( |
||||||
|
$this->doesNotHaveLinkHeader('related'), |
||||||
|
"Node that does not have entity reference field must not return related link header." |
||||||
|
); |
||||||
|
|
||||||
|
// Visit the referenced node, there should not be a header since its |
||||||
|
// entity reference field is empty. |
||||||
|
$this->drupalGet('node/' . $this->referenced->id()); |
||||||
|
$this->assertTrue( |
||||||
|
$this->doesNotHaveLinkHeader('related'), |
||||||
|
"Node that has empty entity reference field must not return link header." |
||||||
|
); |
||||||
|
|
||||||
|
// Visit the referencer. It should return a rel="related" link header |
||||||
|
// for both the referenced node and media entity. |
||||||
|
$this->drupalGet('node/' . $this->referencer->id()); |
||||||
|
$this->assertTrue( |
||||||
|
$this->validateLinkHeader('related', $this->referenced, 'Referenced Entity') == 1, |
||||||
|
"Malformed related link header" |
||||||
|
); |
||||||
|
$this->assertTrue( |
||||||
|
$this->validateLinkHeader('related', $this->media, 'Media Entity') == 1, |
||||||
|
"Malformed related link header" |
||||||
|
); |
||||||
|
|
||||||
|
// Log in as anonymous. |
||||||
|
$account = $this->drupalCreateUser(); |
||||||
|
$this->drupalLogin($account); |
||||||
|
|
||||||
|
// Visit the referencer. It should return a rel="related" link header |
||||||
|
// for both the referenced node and media entity. |
||||||
|
$this->drupalGet('node/' . $this->referencer->id()); |
||||||
|
$this->assertTrue( |
||||||
|
$this->validateLinkHeader('related', $this->referenced, 'Referenced Entity') == 1, |
||||||
|
"Malformed related link header" |
||||||
|
); |
||||||
|
$this->assertTrue( |
||||||
|
$this->validateLinkHeader('related', $this->media, 'Media Entity') == 0, |
||||||
|
"Anonymous should not be able to see media link header" |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue