Commit 0bc54981 authored by effulgentsia's avatar effulgentsia

Issue #2113345 by dawehner, tedbow, Wim Leers, kristiaanvandeneynde, Crell,...

Issue #2113345 by dawehner, tedbow, Wim Leers, kristiaanvandeneynde, Crell, Grayside, klausi: Define a mechanism for custom link relationships
parent 701bee40
This diff is collapsed.
......@@ -464,6 +464,9 @@ services:
http_client_factory:
class: Drupal\Core\Http\ClientFactory
arguments: ['@http_handler_stack']
plugin.manager.link_relation_type:
class: \Drupal\Core\Http\LinkRelationTypeManager
arguments: ['@app.root', '@module_handler', '@cache.discovery']
theme.negotiator:
class: Drupal\Core\Theme\ThemeNegotiator
arguments: ['@access_check.theme']
......
<?php
namespace Drupal\Core\Http;
use Drupal\Core\Plugin\PluginBase;
/**
* Defines a single link relationship type.
*/
class LinkRelationType extends PluginBase implements LinkRelationTypeInterface {
/**
* {@inheritdoc}
*/
public function isRegistered() {
return !$this->isExtension();
}
/**
* {@inheritdoc}
*/
public function isExtension() {
return isset($this->pluginDefinition['uri']);
}
/**
* {@inheritdoc}
*/
public function getRegisteredName() {
return $this->isRegistered() ? $this->getPluginId() : NULL;
}
/**
* {@inheritdoc}
*/
public function getExtensionUri() {
return $this->isExtension() ? $this->pluginDefinition['uri'] : NULL;
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return isset($this->pluginDefinition['description']) ? $this->pluginDefinition['description'] : '';
}
/**
* {@inheritdoc}
*/
public function getReference() {
return isset($this->pluginDefinition['reference']) ? $this->pluginDefinition['reference'] : '';
}
/**
* {@inheritdoc}
*/
public function getNotes() {
return isset($this->pluginDefinition['notes']) ? $this->pluginDefinition['notes'] : '';
}
}
<?php
namespace Drupal\Core\Http;
/**
* Defines a single link relation type.
*
* An example of a link relation type is 'canonical'. It represents a canonical,
* definite representation of a resource.
*
* @see \Drupal\Core\Http\LinkRelationTypeManager
* @see https://tools.ietf.org/html/rfc5988#page-6
*/
interface LinkRelationTypeInterface {
/**
* Indicates whether this link relation type is of the 'registered' kind.
*
* @return bool
*
* @see https://tools.ietf.org/html/rfc5988#section-4.1
*/
public function isRegistered();
/**
* Indicates whether this link relation type is of the 'extension' kind.
*
* @return bool
*
* @see https://tools.ietf.org/html/rfc5988#section-4.2
*/
public function isExtension();
/**
* Returns the registered link relation type name.
*
* Only available for link relation types of the KIND_REGISTERED kind.
*
* @return string|null
* The name of the registered relation type.
*
* @see https://tools.ietf.org/html/rfc5988#section-4.1
*/
public function getRegisteredName();
/**
* Returns the extension link relation type URI.
*
* Only available for link relation types of the KIND_EXTENSION kind.
*
* @return string
* The URI of the extension relation type.
*
* @see https://tools.ietf.org/html/rfc5988#section-4.2
*/
public function getExtensionUri();
/**
* Returns the link relation type description.
*
* @return string
* The link relation type description.
*
* @see https://tools.ietf.org/html/rfc5988#section-6.2.1
*/
public function getDescription();
/**
* Returns the URL pointing to the reference of the link relation type.
*
* @return string
* The URL pointing to the reference.
*
* @see https://tools.ietf.org/html/rfc5988#section-6.2.1
*/
public function getReference();
/**
* Returns some extra notes/comments about this link relation type.
*
* @return string
* The notes about the link relation.
*
* @see https://tools.ietf.org/html/rfc5988#section-6.2.1
*/
public function getNotes();
}
<?php
namespace Drupal\Core\Http;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
/**
* Provides a default plugin manager for link relation types.
*
* @see \Drupal\Core\Http\LinkRelationTypeInterface
*/
class LinkRelationTypeManager extends DefaultPluginManager {
/**
* {@inheritdoc}
*/
protected $defaults = [
'class' => LinkRelationType::class,
];
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Constructs a new LinkRelationTypeManager.
*
* @param string $root
* The app root.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
*/
public function __construct($root, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache) {
$this->root = $root;
$this->pluginInterface = LinkRelationTypeInterface::class;
$this->moduleHandler = $module_handler;
$this->setCacheBackend($cache, 'link_relation_type_plugins', ['link_relation_type']);
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() {
if (!$this->discovery) {
$directories = ['core' => $this->root . '/core'];
$directories += array_map(function (Extension $extension) {
return $this->root . '/' . $extension->getPath();
}, $this->moduleHandler->getModuleList());
$this->discovery = new YamlDiscovery('link_relation_types', $directories);
}
return $this->discovery;
}
}
......@@ -2,7 +2,6 @@
namespace Drupal\Tests\hal\Functional\EntityResource\Comment;
use Drupal\Core\Cache\Cache;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait;
use Drupal\Tests\rest\Functional\EntityResource\Comment\CommentResourceTestBase;
......@@ -128,12 +127,4 @@ protected function getNormalizedPostEntity() {
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
// The 'url.site' cache context is added for '_links' in the response.
return Cache::mergeTags(parent::getExpectedCacheContexts(), ['url.site']);
}
}
......@@ -2,7 +2,6 @@
namespace Drupal\Tests\hal\Functional\EntityResource\EntityTest;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase;
......@@ -89,12 +88,4 @@ protected function getNormalizedPostEntity() {
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
// The 'url.site' cache context is added for '_links' in the response.
return Cache::mergeTags(parent::getExpectedCacheContexts(), ['url.site']);
}
}
......@@ -2,7 +2,6 @@
namespace Drupal\Tests\hal\Functional\EntityResource\Node;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\Node\NodeResourceTestBase;
......@@ -121,12 +120,4 @@ protected function getNormalizedPostEntity() {
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
// The 'url.site' cache context is added for '_links' in the response.
return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['url.site']);
}
}
......@@ -2,7 +2,6 @@
namespace Drupal\Tests\hal\Functional\EntityResource\Term;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\Term\TermResourceTestBase;
......@@ -63,12 +62,4 @@ protected function getNormalizedPostEntity() {
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
// The 'url.site' cache context is added for '_links' in the response.
return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['url.site']);
}
}
......@@ -2,7 +2,6 @@
namespace Drupal\Tests\hal\Functional\EntityResource\User;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\User\UserResourceTestBase;
......@@ -63,12 +62,4 @@ protected function getNormalizedPostEntity() {
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
// The 'url.site' cache context is added for '_links' in the response.
return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['url.site']);
}
}
......@@ -3,6 +3,8 @@
namespace Drupal\rest\Plugin\rest\resource;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
......@@ -14,6 +16,7 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\rest\ModifiedResourceResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
......@@ -53,6 +56,13 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
*/
protected $configFactory;
/**
* The link relation type manager used to create HTTP header links.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $linkRelationTypeManager;
/**
* Constructs a Drupal\rest\Plugin\rest\resource\EntityResource object.
*
......@@ -70,11 +80,14 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
* A logger instance.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Component\Plugin\PluginManagerInterface $link_relation_type_manager
* The link relation type manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, $serializer_formats, LoggerInterface $logger, ConfigFactoryInterface $config_factory) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, $serializer_formats, LoggerInterface $logger, ConfigFactoryInterface $config_factory, PluginManagerInterface $link_relation_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
$this->entityType = $entity_type_manager->getDefinition($plugin_definition['entity_type']);
$this->configFactory = $config_factory;
$this->linkRelationTypeManager = $link_relation_type_manager;
}
/**
......@@ -88,7 +101,8 @@ public static function create(ContainerInterface $container, array $configuratio
$container->get('entity_type.manager'),
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest'),
$container->get('config.factory')
$container->get('config.factory'),
$container->get('plugin.manager.link_relation_type')
);
}
......@@ -125,6 +139,8 @@ public function get(EntityInterface $entity) {
}
}
$this->addLinkHeaders($entity, $response);
return $response;
}
......@@ -340,4 +356,35 @@ public function calculateDependencies() {
}
}
/**
* Adds link headers to a response.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param \Symfony\Component\HttpFoundation\Response $response
* The response.
*
* @see https://tools.ietf.org/html/rfc5988#section-5
*/
protected function addLinkHeaders(EntityInterface $entity, Response $response) {
foreach ($entity->getEntityType()->getLinkTemplates() as $relation_name => $link_template) {
if ($definition = $this->linkRelationTypeManager->getDefinition($relation_name, FALSE)) {
$generator_url = $entity->toUrl($relation_name)
->setAbsolute(TRUE)
->toString(TRUE);
if ($response instanceof CacheableResponseInterface) {
$response->addCacheableDependency($generator_url);
}
$uri = $generator_url->getGeneratedUrl();
$relationship = $relation_name;
if (!empty($definition['uri'])) {
$relationship = $definition['uri'];
}
$link_header = '<' . $uri . '>; rel="' . $relationship . '"';
$response->headers->set('Link', $link_header, FALSE);
}
}
}
}
......@@ -113,7 +113,7 @@ protected function getNormalizedPostEntity() {
*/
protected function getExpectedCacheContexts() {
// @see ::createEntity()
return [];
return ['url.site'];
}
/**
......
......@@ -266,6 +266,7 @@ protected function getExpectedCacheTags() {
*/
protected function getExpectedCacheContexts() {
return [
'url.site',
'user.permissions',
];
}
......@@ -341,6 +342,7 @@ public function testGet() {
$response = $this->request('GET', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertArrayNotHasKey('Link', $response->getHeaders());
$this->setUpAuthorization('GET');
......@@ -379,6 +381,23 @@ public function testGet() {
// response results in the expected object.
$unserialized = $this->serializer->deserialize((string) $response->getBody(), get_class($this->entity), static::$format);
$this->assertSame($unserialized->uuid(), $this->entity->uuid());
// Finally, assert that the expected 'Link' headers are present.
$this->assertArrayHasKey('Link', $response->getHeaders());
$link_relation_type_manager = $this->container->get('plugin.manager.link_relation_type');
$expected_link_relation_headers = array_map(function ($rel) use ($link_relation_type_manager) {
$definition = $link_relation_type_manager->getDefinition($rel, FALSE);
return (!empty($definition['uri']))
? $definition['uri']
: $rel;
}, array_keys($this->entity->getEntityType()->getLinkTemplates()));
$parse_rel_from_link_header = function ($value) use ($link_relation_type_manager) {
$matches = [];
if (preg_match('/rel="([^"]+)"/', $value, $matches) === 1) {
return $matches[1];
}
return FALSE;
};
$this->assertSame($expected_link_relation_headers, array_map($parse_rel_from_link_header, $response->getHeader('Link')));
$get_headers = $response->getHeaders();
// Verify that the GET and HEAD responses are the same. The only difference
......
<?php
namespace Drupal\KernelTests\Core\Http;
use Drupal\Core\Http\LinkRelationType;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests link relationships in Drupal.
*
* @group HTTP
*/
class LinkRelationsTest extends KernelTestBase {
/**
* Tests correct Link Relations are returned from the Link Relation Type Manager.
*/
public function testAvailableLinkRelationships() {
/** @var \Drupal\Core\Http\LinkRelationTypeManager $link_relation_type_manager */
$link_relation_type_manager = $this->container->get('plugin.manager.link_relation_type');
// An link relation type of the "registered" kind.
/** @var \Drupal\Core\Http\LinkRelationTypeInterface $canonical */
$canonical = $link_relation_type_manager->createInstance('canonical');
$this->assertInstanceOf(LinkRelationType::class, $canonical);
$this->assertTrue($canonical->isRegistered());
$this->assertFalse($canonical->isExtension());
$this->assertSame('canonical', $canonical->getRegisteredName());
$this->assertNull($canonical->getExtensionUri());
$this->assertEquals('[RFC6596]', $canonical->getReference());
$this->assertEquals('Designates the preferred version of a resource (the IRI and its contents).', $canonical->getDescription());
$this->assertEquals('', $canonical->getNotes());
// An link relation type of the "extension" kind.
/** @var \Drupal\Core\Http\LinkRelationTypeInterface $canonical */
$add_form = $link_relation_type_manager->createInstance('add-form');
$this->assertInstanceOf(LinkRelationType::class, $add_form);
$this->assertFalse($add_form->isRegistered());
$this->assertTrue($add_form->isExtension());
$this->assertNull($add_form->getRegisteredName());
$this->assertSame('https://drupal.org/link-relations/add-form', $add_form->getExtensionUri());
$this->assertEquals('', $add_form->getReference());
$this->assertEquals('A form where a resource of this type can be created.', $add_form->getDescription());
$this->assertEquals('', $add_form->getNotes());
// Test a couple of examples.
$this->assertContains('about', array_keys($link_relation_type_manager->getDefinitions()));
$this->assertContains('original', array_keys($link_relation_type_manager->getDefinitions()));
$this->assertContains('type', array_keys($link_relation_type_manager->getDefinitions()));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment