Skip to content

Commit

Permalink
Meta link headers (Islandora#81)
Browse files Browse the repository at this point in the history
* Adding rel=alternate link headers for metadata REST endpoints

* Missed a few spots in tests still bootstrapping their own REST config.
  • Loading branch information
dannylamb authored and whikloj committed Mar 20, 2018
1 parent e62f754 commit 6e4815e
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 128 deletions.
4 changes: 2 additions & 2 deletions islandora.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ services:
- { name: event_subscriber }
islandora.media_link_header_subscriber:
class: Drupal\islandora\EventSubscriber\MediaLinkHeaderSubscriber
arguments: ['@entity_field.manager', '@current_route_match', '@entity_type.manager']
arguments: ['@entity_type.manager', '@entity_field.manager', '@current_route_match', '@access_manager', '@current_user']
tags:
- { name: event_subscriber }
islandora.node_link_header_subscriber:
class: Drupal\islandora\EventSubscriber\NodeLinkHeaderSubscriber
arguments: ['@entity_field.manager', '@current_route_match']
arguments: ['@entity_type.manager', '@entity_field.manager', '@current_route_match', '@access_manager', '@current_user']
tags:
- { name: event_subscriber }
islandora.versioncounter:
Expand Down
145 changes: 144 additions & 1 deletion src/EventSubscriber/LinkHeaderSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

namespace Drupal\islandora\EventSubscriber;

use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Entity\EntityFieldManager;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
Expand All @@ -16,6 +21,13 @@
*/
abstract class LinkHeaderSubscriber implements EventSubscriberInterface {

/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManager
*/
protected $entityTypeManager;

/**
* The entity field manager.
*
Expand All @@ -33,17 +45,45 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface {
/**
* Constructor.
*
* The access manager.
*
* @var \Drupal\Core\Access\AccessManagerInterface
*/
protected $accessManager;

/**
* Current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;

/**
* Constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManager $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManager $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager
* The access manager.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*/
public function __construct(
EntityTypeManager $entity_type_manager,
EntityFieldManager $entity_field_manager,
RouteMatchInterface $route_match
RouteMatchInterface $route_match,
AccessManagerInterface $access_manager,
AccountInterface $account
) {
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->routeMatch = $route_match;
$this->accessManager = $access_manager;
$this->account = $account;
}

/**
Expand Down Expand Up @@ -113,6 +153,109 @@ protected function getObject(Response $response, $object_type) {
return $object;
}

/**
* Generates link headers for each referenced entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* Entity that has reference fields.
*
* @return string[]
* Array of link headers
*/
protected function generateEntityReferenceLinks(EntityInterface $entity) {
// Use the node to add link headers for each entity reference.
$entity_type = $entity->getEntityType()->id();
$bundle = $entity->bundle();

// Get all fields for the entity.
$fields = $this->entityFieldManager->getFieldDefinitions($entity_type, $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 ($entity->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\"";
}
}
}

return $links;
}

/**
* Generates link headers for REST endpoints.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* Entity that has reference fields.
*
* @return string[]
* Array of link headers
*/
protected function generateRestLinks(EntityInterface $entity) {
$rest_resource_config_storage = $this->entityTypeManager->getStorage('rest_resource_config');
$entity_type = $entity->getEntityType()->id();
$rest_resource_config = $rest_resource_config_storage->load("entity.$entity_type");

$links = [];
$route_name = $this->routeMatch->getRouteName();

if ($rest_resource_config) {
$configuration = $rest_resource_config->get('configuration');

foreach ($configuration['GET']['supported_formats'] as $format) {
switch ($format) {
case 'json':
$mime = 'application/json';
break;

case 'jsonld':
$mime = 'application/ld+json';
break;

case 'hal_json':
$mime = 'application/hal+json';
break;

case 'xml':
$mime = 'application/xml';
break;

default:
continue;
}

$meta_route_name = "rest.entity.$entity_type.GET.$format";

if ($route_name == $meta_route_name) {
continue;
}

$route_params = [$entity_type => $entity->id()];

if (!$this->accessManager->checkNamedRoute($meta_route_name, $route_params, $this->account)) {
continue;
}

$meta_url = Url::fromRoute($meta_route_name, $route_params)
->setAbsolute()
->toString();

$links[] = "<$meta_url?_format=$format>; rel=\"alternate\"; type=\"$mime\"";
}
}

return $links;
}

/**
* Adds resource-specific link headers to appropriate responses.
*
Expand Down
96 changes: 45 additions & 51 deletions src/EventSubscriber/MediaLinkHeaderSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@

namespace Drupal\islandora\EventSubscriber;

use Drupal\Core\Entity\EntityFieldManager;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\media_entity\MediaInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

Expand All @@ -17,80 +14,77 @@
*/
class MediaLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSubscriberInterface {

/**
* Media storage interface.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $mediaBundleStorage;

/**
* MediaLinkHeaderSubscriber constructor.
*
* @param \Drupal\Core\Entity\EntityFieldManager $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(
EntityFieldManager $entity_field_manager,
RouteMatchInterface $route_match,
EntityTypeManagerInterface $entity_type_manager) {
$this->mediaBundleStorage = $entity_type_manager->getStorage('media_bundle');
parent::__construct($entity_field_manager, $route_match);
}

/**
* {@inheritdoc}
*/
public function onResponse(FilterResponseEvent $event) {
$response = $event->getResponse();

$entity = $this->getObject($response, 'media');
$media = $this->getObject($response, 'media');

if ($entity === FALSE) {
if ($media === FALSE) {
return;
}

$media_bundle = $this->mediaBundleStorage->load($entity->bundle());
$links = array_merge(
$this->generateEntityReferenceLinks($media),
$this->generateRestLinks($media),
$this->generateMediaLinks($media)
);

// Add the link headers to the response.
if (empty($links)) {
return;
}

$response->headers->set('Link', $links, FALSE);
}

/**
* Generates link headers for the described file and source update routes.
*
* @param \Drupal\media_entity\MediaInterface $media
* Media to generate link headers.
*
* @return string[]
* Array of link headers
*/
protected function generateMediaLinks(MediaInterface $media) {
$media_bundle = $this->entityTypeManager->getStorage('media_bundle')->load($media->bundle());

$type_configuration = $media_bundle->getTypeConfiguration();

$links = [];

$update_route_name = 'islandora.media_source_update';
$update_route_params = ['media' => $media->id()];
if ($this->accessManager->checkNamedRoute($update_route_name, $update_route_params, $this->account)) {
$edit_media_url = Url::fromRoute($update_route_name, $update_route_params)
->setAbsolute()
->toString();
$links[] = "<$edit_media_url>; rel=\"edit-media\"";
}

if (!isset($type_configuration['source_field'])) {
return;
return $links;
}
$source_field = $type_configuration['source_field'];

if (empty($source_field) ||
!$entity instanceof FieldableEntityInterface ||
!$entity->hasField($source_field)
!$media->hasField($source_field)
) {
return;
return $links;
}

// Collect file links for the media.
$links = [];
foreach ($entity->get($source_field)->referencedEntities() as $referencedEntity) {
if ($entity->access('view')) {
foreach ($media->get($source_field)->referencedEntities() as $referencedEntity) {
if ($referencedEntity->access('view')) {
$file_url = $referencedEntity->url('canonical', ['absolute' => TRUE]);
$edit_media_url = Url::fromRoute('islandora.media_source_update', ['media' => $referencedEntity->id()])
->setAbsolute()
->toString();
$links[] = "<$file_url>; rel=\"describes\"; type=\"{$referencedEntity->getMimeType()}\"";
$links[] = "<$edit_media_url>; rel=\"edit-media\"";
}
}

// Exit early if there aren't any.
if (empty($links)) {
return;
}

// Add the link headers to the response.
$response->headers->set('Link', $links, FALSE);

return $links;
}

}
34 changes: 7 additions & 27 deletions src/EventSubscriber/NodeLinkHeaderSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,42 +21,22 @@ class NodeLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSubs
public function onResponse(FilterResponseEvent $event) {
$response = $event->getResponse();

$entity = $this->getObject($response, 'node');
$node = $this->getObject($response, 'node');

if ($entity === FALSE) {
if ($node === FALSE) {
return;
}

// Use the node to add link headers for each entity reference.
$bundle = $entity->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 ($entity->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\"";
}
}
}
$links = array_merge(
$this->generateEntityReferenceLinks($node),
$this->generateRestLinks($node)
);

// Exit early if there aren't any.
// Add the link headers to the response.
if (empty($links)) {
return;
}

// Add the link headers to the response.
$response->headers->set('Link', $links, FALSE);
}

Expand Down
Loading

0 comments on commit 6e4815e

Please sign in to comment.