Browse Source

Add headers to media entities (#72)

* Add media headers

* Add and fix up tests

* Rebase and code review

* Remove ?_format=jsonld from edit-media link
pull/756/head
Jared Whiklo 7 years ago committed by Natkeeran
parent
commit
857e4d8252
  1. 5
      islandora.services.yml
  2. 124
      src/EventSubscriber/LinkHeaderSubscriber.php
  3. 96
      src/EventSubscriber/MediaLinkHeaderSubscriber.php
  4. 83
      src/EventSubscriber/NodeLinkHeaderSubscriber.php
  5. 57
      tests/src/Functional/IslandoraFunctionalTestBase.php
  6. 38
      tests/src/Functional/MediaLinkHeaderTest.php
  7. 3
      tests/src/Functional/MediaSourceUpdateTest.php
  8. 8
      tests/src/Functional/RelatedLinkHeaderTest.php

5
islandora.services.yml

@ -18,6 +18,11 @@ services:
arguments: ['@entity_type.manager', '@current_user'] arguments: ['@entity_type.manager', '@current_user']
tags: tags:
- { name: event_subscriber } - { name: event_subscriber }
islandora.media_link_header_subscriber:
class: Drupal\islandora\EventSubscriber\MediaLinkHeaderSubscriber
arguments: ['@entity_field.manager', '@current_route_match', '@entity_type.manager']
tags:
- { name: event_subscriber }
islandora.node_link_header_subscriber: islandora.node_link_header_subscriber:
class: Drupal\islandora\EventSubscriber\NodeLinkHeaderSubscriber class: Drupal\islandora\EventSubscriber\NodeLinkHeaderSubscriber
arguments: ['@entity_field.manager', '@current_route_match'] arguments: ['@entity_field.manager', '@current_route_match']

124
src/EventSubscriber/LinkHeaderSubscriber.php

@ -0,0 +1,124 @@
<?php
namespace Drupal\islandora\EventSubscriber;
use Drupal\Core\Entity\EntityFieldManager;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Class LinkHeaderSubscriber.
*
* @package Drupal\islandora\EventSubscriber
*/
abstract class LinkHeaderSubscriber 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;
}
/**
* Get the Node | Media | File.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* The current response object.
* @param string $object_type
* The type of entity to look for.
*
* @return Drupal\Core\Entity\ContentEntityBase|bool
* A node or media entity or FALSE if we should skip out.
*/
protected function getObject(Response $response, $object_type) {
if ($object_type != 'node'
&& $object_type != 'media'
) {
return FALSE;
}
// Exit early if the response is already cached.
if ($response->headers->get('X-Drupal-Dynamic-Cache') == 'HIT') {
return FALSE;
}
if (!$response->isOk()) {
return FALSE;
}
// Hack the node out of the route.
$route_object = $this->routeMatch->getRouteObject();
if (!$route_object) {
return FALSE;
}
$methods = $route_object->getMethods();
$is_get = in_array('GET', $methods);
$is_head = in_array('HEAD', $methods);
if (!($is_get || $is_head)) {
return FALSE;
}
$route_contexts = $route_object->getOption('parameters');
if (!$route_contexts) {
return FALSE;
}
if (!isset($route_contexts[$object_type])) {
return FALSE;
}
$object = $this->routeMatch->getParameter($object_type);
if (!$object) {
return FALSE;
}
return $object;
}
/**
* Adds resource-specific link headers to appropriate responses.
*
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
* Event containing the response.
*/
abstract public function onResponse(FilterResponseEvent $event);
}

96
src/EventSubscriber/MediaLinkHeaderSubscriber.php

@ -0,0 +1,96 @@
<?php
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 Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
/**
* Class MediaLinkHeaderSubscriber.
*
* @package Drupal\islandora\EventSubscriber
*/
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');
if ($entity === FALSE) {
return;
}
$media_bundle = $this->mediaBundleStorage->load($entity->bundle());
$type_configuration = $media_bundle->getTypeConfiguration();
if (!isset($type_configuration['source_field'])) {
return;
}
$source_field = $type_configuration['source_field'];
if (empty($source_field) ||
!$entity instanceof FieldableEntityInterface ||
!$entity->hasField($source_field)
) {
return;
}
// Collect file links for the media.
$links = [];
foreach ($entity->get($source_field)->referencedEntities() as $referencedEntity) {
if ($entity->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);
}
}

83
src/EventSubscriber/NodeLinkHeaderSubscriber.php

@ -2,58 +2,15 @@
namespace Drupal\islandora\EventSubscriber; namespace Drupal\islandora\EventSubscriber;
use Drupal\Core\Entity\EntityFieldManager;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/** /**
* Class NodeLinkHeaderSubscriber. * Class NodeLinkHeaderSubscriber.
* *
* @package Drupal\islandora\EventSubscriber * @package Drupal\islandora\EventSubscriber
*/ */
class NodeLinkHeaderSubscriber implements EventSubscriberInterface { class NodeLinkHeaderSubscriber extends LinkHeaderSubscriber 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. * Adds node-specific link headers to appropriate responses.
@ -64,44 +21,14 @@ class NodeLinkHeaderSubscriber implements EventSubscriberInterface {
public function onResponse(FilterResponseEvent $event) { public function onResponse(FilterResponseEvent $event) {
$response = $event->getResponse(); $response = $event->getResponse();
// Exit early if the response is already cached. $entity = $this->getObject($response, 'node');
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) { if ($entity === FALSE) {
return; return;
} }
// Use the node to add link headers for each entity reference. // Use the node to add link headers for each entity reference.
$bundle = $node->bundle(); $bundle = $entity->bundle();
// Get all fields for the entity. // Get all fields for the entity.
$fields = $this->entityFieldManager->getFieldDefinitions('node', $bundle); $fields = $this->entityFieldManager->getFieldDefinitions('node', $bundle);
@ -114,7 +41,7 @@ class NodeLinkHeaderSubscriber implements EventSubscriberInterface {
// Collect links for referenced entities. // Collect links for referenced entities.
$links = []; $links = [];
foreach ($entity_reference_fields as $field_name => $field_definition) { foreach ($entity_reference_fields as $field_name => $field_definition) {
foreach ($node->get($field_name)->referencedEntities() as $referencedEntity) { foreach ($entity->get($field_name)->referencedEntities() as $referencedEntity) {
// Headers are subject to an access check. // Headers are subject to an access check.
if ($referencedEntity->access('view')) { if ($referencedEntity->access('view')) {
$entity_url = $referencedEntity->url('canonical', ['absolute' => TRUE]); $entity_url = $referencedEntity->url('canonical', ['absolute' => TRUE]);

57
tests/src/Functional/IslandoraFunctionalTestBase.php

@ -3,6 +3,7 @@
namespace Drupal\Tests\islandora\Functional; namespace Drupal\Tests\islandora\Functional;
use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase; use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\TestFileCreationTrait; use Drupal\Tests\TestFileCreationTrait;
@ -56,6 +57,7 @@ class IslandoraFunctionalTestBase extends BrowserTestBase {
]); ]);
$hello_world->save(); $hello_world->save();
$this->container->get('router.builder')->rebuild();
} }
/** /**
@ -94,7 +96,7 @@ class IslandoraFunctionalTestBase extends BrowserTestBase {
$file = current($this->getTestFiles('image')); $file = current($this->getTestFiles('image'));
$values = [ $values = [
'name[0][value]' => 'Test Media', 'name[0][value]' => 'Test Media',
'files[field_image_0]' => drupal_realpath($file->uri), 'files[field_image_0]' => \Drupal::service('file_system')->realpath($file->uri),
]; ];
$this->drupalPostForm('media/add/tn', $values, t('Save and publish')); $this->drupalPostForm('media/add/tn', $values, t('Save and publish'));
$values = [ $values = [
@ -102,8 +104,20 @@ class IslandoraFunctionalTestBase extends BrowserTestBase {
]; ];
$this->getSession()->getPage()->fillField('edit-field-image-0-alt', 'alt text'); $this->getSession()->getPage()->fillField('edit-field-image-0-alt', 'alt text');
$this->getSession()->getPage()->pressButton(t('Save and publish')); $this->getSession()->getPage()->pressButton(t('Save and publish'));
$this->assertResponse(200); $this->assertSession()->statusCodeEquals(200);
return $this->getUrl(); $results = $this->container->get('entity_type.manager')->getStorage('file')->loadByProperties(['filename' => $file->filename]);
$file_entity = reset($results);
$file_url = $file_entity->url('canonical', ['absolute' => TRUE]);
$rest_url = Url::fromRoute('islandora.media_source_update', ['media' => $file_entity->id()])
->setAbsolute()
->toString();
return [
'media' => $this->getUrl(),
'file' => [
'file' => $file_url,
'rest' => $rest_url,
],
];
} }
/** /**
@ -111,7 +125,7 @@ class IslandoraFunctionalTestBase extends BrowserTestBase {
*/ */
protected function postNodeAddForm($bundle_id, $values, $button_text) { protected function postNodeAddForm($bundle_id, $values, $button_text) {
$this->drupalPostForm("node/add/$bundle_id", $values, t('@text', ['@text' => $button_text])); $this->drupalPostForm("node/add/$bundle_id", $values, t('@text', ['@text' => $button_text]));
$this->assertResponse(200); $this->assertSession()->statusCodeEquals(200);
} }
/** /**
@ -119,7 +133,7 @@ class IslandoraFunctionalTestBase extends BrowserTestBase {
*/ */
protected function postEntityEditForm($entity_url, $values, $button_text) { protected function postEntityEditForm($entity_url, $values, $button_text) {
$this->drupalPostForm("$entity_url/edit", $values, t('@text', ['@text' => $button_text])); $this->drupalPostForm("$entity_url/edit", $values, t('@text', ['@text' => $button_text]));
$this->assertResponse(200); $this->assertSession()->statusCodeEquals(200);
} }
/** /**
@ -144,7 +158,7 @@ class IslandoraFunctionalTestBase extends BrowserTestBase {
} }
/** /**
* Checks if the collection link header contains the correct uri. * Checks if the correct link header exists for an Entity.
* *
* @param string $rel * @param string $rel
* The expected relation type of the link header. * The expected relation type of the link header.
@ -152,17 +166,42 @@ class IslandoraFunctionalTestBase extends BrowserTestBase {
* The entity whose uri is expected in the link header. * The entity whose uri is expected in the link header.
* @param string $title * @param string $title
* The expected title of the link header. * The expected title of the link header.
* @param string $type
* The expected mimetype for the link header.
*
* @return int
* The number of times the correct header appears.
*/
protected function validateLinkHeaderWithEntity($rel, EntityInterface $entity, $title = '', $type = '') {
$entity_url = $entity->toUrl('canonical', ['absolute' => TRUE])
->toString();
return $this->validateLinkHeaderWithUrl($rel, $entity_url, $title, $type);
}
/**
* Checks if the correct link header exists for a string URI.
*
* @param string $rel
* The expected relation type of the link header.
* @param string $url
* The uri is expected in the link header.
* @param string $title
* The expected title of the link header.
* @param string $type
* The expected mimetype for the link header.
* *
* @return int * @return int
* The number of times the correct header appears. * The number of times the correct header appears.
*/ */
protected function validateLinkHeader($rel, EntityInterface $entity, $title = '') { protected function validateLinkHeaderWithUrl($rel, $url, $title = '', $type = '') {
$entity_url = $entity->url('canonical', ['absolute' => TRUE]);
$regex = '/<(.*)>; rel="' . preg_quote($rel) . '"'; $regex = '/<(.*)>; rel="' . preg_quote($rel) . '"';
if (!empty($title)) { if (!empty($title)) {
$regex .= '; title="' . preg_quote($title) . '"'; $regex .= '; title="' . preg_quote($title) . '"';
} }
if (!empty($type)) {
$regex .= '; type="' . preg_quote($type, '/') . '"';
}
$regex .= '/'; $regex .= '/';
$count = 0; $count = 0;
@ -173,7 +212,7 @@ class IslandoraFunctionalTestBase extends BrowserTestBase {
$split = explode(',', $link_headers); $split = explode(',', $link_headers);
foreach ($split as $link_header) { foreach ($split as $link_header) {
$matches = []; $matches = [];
if (preg_match($regex, $link_header, $matches) && $matches[1] == $entity_url) { if (preg_match($regex, $link_header, $matches) && $matches[1] == $url) {
$count++; $count++;
} }
} }

38
tests/src/Functional/MediaLinkHeaderTest.php

@ -0,0 +1,38 @@
<?php
namespace Drupal\Tests\islandora\Functional;
/**
* Tests the MediaLinkHeader event subscriber.
*
* @group islandora
*/
class MediaLinkHeaderTest extends IslandoraFunctionalTestBase {
/**
* @covers \Drupal\islandora\EventSubscriber\MediaLinkHeaderSubscriber
*/
public function testMediaLinkHeaders() {
// Create a test user.
$account = $this->drupalCreateUser([
'view media',
'create media',
'update media',
]);
$this->drupalLogin($account);
$urls = $this->createThumbnailWithFile();
$this->drupalGet($urls['media'], [], ['Cache-Control: no-cache']);
$this->assertTrue(
$this->validateLinkHeaderWithUrl('describes', $urls['file']['file'], '', 'image/png') == 1,
"Malformed 'describes' link header"
);
$this->assertTrue(
$this->validateLinkHeaderWithUrl('edit-media', $urls['file']['rest'], '', '') == 1,
"Malformed 'edit-media' link header"
);
}
}

3
tests/src/Functional/MediaSourceUpdateTest.php

@ -44,7 +44,8 @@ class MediaSourceUpdateTest extends IslandoraFunctionalTestBase {
$this->drupalLogin($account); $this->drupalLogin($account);
// Make a media and give it a png. // Make a media and give it a png.
$url = $this->createThumbnailWithFile(); $urls = $this->createThumbnailWithFile();
$url = $urls['media'];
// Hack out the guzzle client. // Hack out the guzzle client.
$client = $this->getSession()->getDriver()->getClient()->getClient(); $client = $this->getSession()->getDriver()->getClient()->getClient();

8
tests/src/Functional/RelatedLinkHeaderTest.php

@ -119,11 +119,11 @@ class RelatedLinkHeaderTest extends IslandoraFunctionalTestBase {
// for both the referenced node and media entity. // for both the referenced node and media entity.
$this->drupalGet('node/' . $this->referencer->id()); $this->drupalGet('node/' . $this->referencer->id());
$this->assertTrue( $this->assertTrue(
$this->validateLinkHeader('related', $this->referenced, 'Referenced Entity') == 1, $this->validateLinkHeaderWithEntity('related', $this->referenced, 'Referenced Entity') == 1,
"Malformed related link header" "Malformed related link header"
); );
$this->assertTrue( $this->assertTrue(
$this->validateLinkHeader('related', $this->media, 'Media Entity') == 1, $this->validateLinkHeaderWithEntity('related', $this->media, 'Media Entity') == 1,
"Malformed related link header" "Malformed related link header"
); );
@ -135,11 +135,11 @@ class RelatedLinkHeaderTest extends IslandoraFunctionalTestBase {
// for both the referenced node and media entity. // for both the referenced node and media entity.
$this->drupalGet('node/' . $this->referencer->id()); $this->drupalGet('node/' . $this->referencer->id());
$this->assertTrue( $this->assertTrue(
$this->validateLinkHeader('related', $this->referenced, 'Referenced Entity') == 1, $this->validateLinkHeaderWithEntity('related', $this->referenced, 'Referenced Entity') == 1,
"Malformed related link header" "Malformed related link header"
); );
$this->assertTrue( $this->assertTrue(
$this->validateLinkHeader('related', $this->media, 'Media Entity') == 0, $this->validateLinkHeaderWithEntity('related', $this->media, 'Media Entity') == 0,
"Anonymous should not be able to see media link header" "Anonymous should not be able to see media link header"
); );
} }

Loading…
Cancel
Save