summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreffulgentsia2017-01-30 12:29:33 -0800
committereffulgentsia2017-01-30 12:29:33 -0800
commit0bc54981d222787f508b586dcb420eadc6b6aa6f (patch)
tree0d20854b83b3cffa60318ee2a42b433faac5da12
parent701bee40ef335f69088efa5d50b2820155b0d005 (diff)
Issue #2113345 by dawehner, tedbow, Wim Leers, kristiaanvandeneynde, Crell, Grayside, klausi: Define a mechanism for custom link relationships
-rw-r--r--core/core.link_relation_types.yml287
-rw-r--r--core/core.services.yml3
-rw-r--r--core/lib/Drupal/Core/Http/LinkRelationType.php61
-rw-r--r--core/lib/Drupal/Core/Http/LinkRelationTypeInterface.php88
-rw-r--r--core/lib/Drupal/Core/Http/LinkRelationTypeManager.php63
-rw-r--r--core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php9
-rw-r--r--core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php9
-rw-r--r--core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php9
-rw-r--r--core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php9
-rw-r--r--core/modules/hal/tests/src/Functional/EntityResource/User/UserHalJsonAnonTest.php9
-rw-r--r--core/modules/rest/src/Plugin/rest/resource/EntityResource.php51
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php2
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php19
-rw-r--r--core/tests/Drupal/KernelTests/Core/Http/LinkRelationsTest.php52
14 files changed, 623 insertions, 48 deletions
diff --git a/core/core.link_relation_types.yml b/core/core.link_relation_types.yml
new file mode 100644
index 0000000..f53c72d
--- /dev/null
+++ b/core/core.link_relation_types.yml
@@ -0,0 +1,287 @@
+# Drupal core's extension relation types.
+# See https://tools.ietf.org/html/rfc5988#section-4.2.
+add-form:
+ uri: https://drupal.org/link-relations/add-form
+ description: A form where a resource of this type can be created.
+delete-form:
+ uri: https://drupal.org/link-relations/delete-form
+ description: A form where a resource of this type can be deleted.
+revision:
+ uri: https://drupal.org/link-relations/revision
+ description: A particular version of this resource.
+create:
+ uri: https://drupal.org/link-relations/create
+ description: A REST resource URL where a resource of this type can be created.
+enable:
+ uri: https://drupal.org/link-relations/enable
+ description: A REST resource URL where a resource of this type can be enabled.
+disable:
+ uri: https://drupal.org/link-relations/disable
+ description: A REST resource URL where a resource of this type can be disabled.
+edit-permissions-form:
+ uri: https://drupal.org/link-relations/edit-permissions-form
+ description: A form where permissions assigned to a resource of this type can be edited.
+overview-form:
+ uri: https://drupal.org/link-relations/overview-form
+ description: A form where an overview of the collection of resources belonging to a resource of this type can be edited in bulk.
+reset-form:
+ uri: https://drupal.org/link-relations/reset-form
+ description: A form where an overview of the collection of resources belonging to a resource of this type can be reset.
+cancel-form:
+ uri: https://drupal.org/link-relations/cancel-form
+ description: A form where a resource of this type can be canceled.
+
+# All registered relation types.
+# See https://tools.ietf.org/html/rfc5988#section-4.1.
+# See https://www.iana.org/assignments/link-relations/link-relations.xhtml.
+about:
+ description: "Refers to a resource that is the subject of the link's context."
+ reference: '[RFC6903], section 2'
+alternate:
+ description: 'Refers to a substitute for this context'
+ reference: '[http://www.w3.org/TR/html5/links.html#link-type-alternate]'
+appendix:
+ description: 'Refers to an appendix.'
+ reference: '[http://www.w3.org/TR/1999/REC-html401-19991224]'
+archives:
+ description: "Refers to a collection of records, documents, or other materials of historical interest."
+ reference: '[http://www.w3.org/TR/2011/WD-html5-20110113/links.html#rel-archives]'
+author:
+ description: "Refers to the context's author."
+ reference: '[http://www.w3.org/TR/html5/links.html#link-type-author]'
+blocked-by:
+ description: "Identifies the entity that blocks access to a resource following receipt of a legal demand."
+ reference: '[RFC7725]'
+bookmark:
+ description: 'Gives a permanent link to use for bookmarking purposes.'
+ reference: '[http://www.w3.org/TR/html5/links.html#link-type-bookmark]'
+canonical:
+ description: 'Designates the preferred version of a resource (the IRI and its contents).'
+ reference: '[RFC6596]'
+chapter:
+ description: 'Refers to a chapter in a collection of resources.'
+ reference: '[http://www.w3.org/TR/1999/REC-html401-19991224]'
+collection:
+ description: 'The target IRI points to a resource which represents the collection resource for the context IRI.'
+ reference: '[RFC6573]'
+contents:
+ description: 'Refers to a table of contents.'
+ reference: '[http://www.w3.org/TR/1999/REC-html401-19991224]'
+copyright:
+ description: "Refers to a copyright statement that applies to the link's context."
+ reference: '[http://www.w3.org/TR/1999/REC-html401-19991224]'
+create-form:
+ description: 'The target IRI points to a resource where a submission form can be obtained.'
+ reference: '[RFC6861]'
+current:
+ description: "Refers to a resource containing the most recent item(s) in a collection of resources."
+ reference: '[RFC5005]'
+derivedfrom:
+ description: 'The target IRI points to a resource from which this material was derived.'
+ reference: '[draft-hoffman-xml2rfc]'
+describedby:
+ description: "Refers to a resource providing information about the link's context."
+ reference: '[http://www.w3.org/TR/powder-dr/#assoc-linking]'
+describes:
+ description: "The relationship A 'describes' B asserts that resource A provides a description of resource B. There are no constraints on the format or representation of either A or B, neither are there any further constraints on either resource."
+ reference: '[RFC6892]'
+ notes: "This link relation type is the inverse of the 'describedby' relation type. While 'describedby' establishes a relation from the described resource back to the resource that describes it, 'describes' established a relation from the describing resource to the resource it describes. If B is 'describedby' A, then A 'describes' B."
+disclosure:
+ description: "Refers to a list of patent disclosures made with respect to material for which 'disclosure' relation is specified."
+ reference: '[RFC6579]'
+dns-prefetch:
+ description: "Used to indicate an origin that will be used to fetch required resources for the link context, and that the user agent ought to resolve as early as possible."
+ reference: '[https://www.w3.org/TR/resource-hints/]'
+duplicate:
+ description: "Refers to a resource whose available representations are byte-for-byte identical with the corresponding representations of the context IRI."
+ reference: '[RFC6249]'
+ notes: "This relation is for static resources. That is, an HTTP GET request on any duplicate will return the same representation. It does not make sense for dynamic or POSTable resources and should not be used for them."
+edit:
+ description: "Refers to a resource that can be used to edit the link's context."
+ reference: '[RFC5023]'
+edit-form:
+ description: "The target IRI points to a resource where a submission form for editing associated resource can be obtained."
+ reference: '[RFC6861]'
+edit-media:
+ description: "Refers to a resource that can be used to edit media associated with the link's context."
+ reference: '[RFC5023]'
+enclosure:
+ description: "Identifies a related resource that is potentially large and might require special handling."
+ reference: '[RFC4287]'
+first:
+ description: "An IRI that refers to the furthest preceding resource in a series of resources."
+ reference: '[RFC5988]'
+ notes: "This relation type registration did not indicate a reference. Originally requested by Mark Nottingham in December 2004."
+glossary:
+ description: 'Refers to a glossary of terms.'
+ reference: '[http://www.w3.org/TR/1999/REC-html401-19991224]'
+help:
+ description: 'Refers to context-sensitive help.'
+ reference: '[http://www.w3.org/TR/html5/links.html#link-type-help]'
+hosts:
+ description: "Refers to a resource hosted by the server indicated by the link context."
+ reference: '[RFC6690]'
+ notes: "This relation is used in CoRE where links are retrieved as a \"/.well-known/core\" resource representation, and is the default relation type in the CoRE Link Format."
+hub:
+ description: "Refers to a hub that enables registration for notification of updates to the context."
+ reference: '[http://pubsubhubbub.googlecode.com]'
+ notes: 'This relation type was requested by Brett Slatkin.'
+icon:
+ description: "Refers to an icon representing the link's context."
+ reference: '[http://www.w3.org/TR/html5/links.html#link-type-icon]'
+index:
+ description: 'Refers to an index.'
+ reference: '[http://www.w3.org/TR/1999/REC-html401-19991224]'
+item:
+ description: 'The target IRI points to a resource that is a member of the collection represented by the context IRI.'
+ reference: '[RFC6573]'
+last:
+ description: "An IRI that refers to the furthest following resource in a series of resources."
+ reference: '[RFC5988]'
+ notes: "This relation type registration did not indicate a reference. Originally requested by Mark Nottingham in December 2004."
+latest-version:
+ description: "Points to a resource containing the latest (e.g., current) version of the context."
+ reference: '[RFC5829]'
+license:
+ description: 'Refers to a license associated with this context.'
+ reference: '[RFC4946]'
+ notes: "For implications of use in HTML, see: http://www.w3.org/TR/html5/links.html#link-type-license"
+lrdd:
+ description: "Refers to further information about the link's context, expressed as a LRDD (\"Link-based Resource Descriptor Document\") resource. See [RFC6415] for information about processing this relation type in host-meta documents. When used elsewhere, it refers to additional links and other metadata. Multiple instances indicate additional LRDD resources. LRDD resources MUST have an \"application/xrd+xml\" representation, and MAY have others."
+ reference: '[RFC6415]'
+memento:
+ description: 'The Target IRI points to a Memento, a fixed resource that will not change state anymore.'
+ reference: '[RFC7089]'
+ notes: "A Memento for an Original Resource is a resource that encapsulates a prior state of the Original Resource."
+monitor:
+ description: 'Refers to a resource that can be used to monitor changes in an HTTP resource.'
+ reference: '[RFC5989]'
+monitor-group:
+ description: 'Refers to a resource that can be used to monitor changes in a specified group of HTTP resources.'
+ reference: '[RFC5989]'
+next:
+ description: "Indicates that the link's context is a part of a series, and that the next in the series is the link target."
+ reference: '[http://www.w3.org/TR/html5/links.html#link-type-next]'
+next-archive:
+ description: 'Refers to the immediately following archive resource.'
+ reference: '[RFC5005]'
+nofollow:
+ description: 'Indicates that the context’s original author or publisher does not endorse the link target.'
+ reference: '[http://www.w3.org/TR/html5/links.html#link-type-nofollow]'
+noreferrer:
+ description: 'Indicates that no referrer information is to be leaked when following the link.'
+ reference: '[http://www.w3.org/TR/html5/links.html#link-type-noreferrer]'
+original:
+ description: 'The Target IRI points to an Original Resource.'
+ reference: '[RFC7089]'
+ notes: "An Original Resource is a resource that exists or used to exist, and for which access to one of its prior states may be required."
+payment:
+ description: 'Indicates a resource where payment is accepted.'
+ reference: '[RFC5988]'
+ notes: "This relation type registration did not indicate a reference. Requested by Joshua Kinberg and Robert Sayre. It is meant as a general way to facilitate acts of payment, and thus this specification makes no assumptions on the type of payment or transaction protocol. Examples may include a web page where donations are accepted or where goods and services are available for purchase. rel=\"payment\" is not intended to initiate an automated transaction. In Atom documents, a link element with a rel=\"payment\" attribute may exist at the feed/channel level and/or the entry/item level. For example, a rel=\"payment\" link at the feed/channel level may point to a \"tip jar\" URI, whereas an entry/ item containing a book review may include a rel=\"payment\" link that points to the location where the book may be purchased through an online retailer."
+pingback:
+ description: 'Gives the address of the pingback resource for the link context.'
+ reference: '[http://www.hixie.ch/specs/pingback/pingback]'
+preconnect:
+ description: "Used to indicate an origin that will be used to fetch required resources for the link context. Initiating an early connection, which includes the DNS lookup, TCP handshake, and optional TLS negotiation, allows the user agent to mask the high latency costs of establishing a connection."
+ reference: '[https://www.w3.org/TR/resource-hints/]'
+predecessor-version:
+ description: "Points to a resource containing the predecessor version in the version history."
+ reference: '[RFC5829]'
+prefetch:
+ description: "The prefetch link relation type is used to identify a resource that might be required by the next navigation from the link context, and that the user agent ought to fetch, such that the user agent can deliver a faster response once the resource is requested in the future."
+ reference: '[http://www.w3.org/TR/resource-hints/]'
+preload:
+ description: "Refers to a resource that should be loaded early in the processing of the link's context, without blocking rendering."
+ reference: '[http://www.w3.org/TR/preload/]'
+ notes: 'Additional target attributes establish the detailed fetch properties of the link.'
+prerender:
+ description: "Used to identify a resource that might be required by the next navigation from the link context, and that the user agent ought to fetch and execute, such that the user agent can deliver a faster response once the resource is requested in the future."
+ reference: '[https://www.w3.org/TR/resource-hints/]'
+prev:
+ description: "Indicates that the link's context is a part of a series, and that the previous in the series is the link target."
+ reference: '[http://www.w3.org/TR/html5/links.html#link-type-prev]'
+preview:
+ description: "Refers to a resource that provides a preview of the link's context."
+ reference: '[RFC6903], section 3'
+previous:
+ description: "Refers to the previous resource in an ordered series of resources. Synonym for \"prev\"."
+ reference: '[http://www.w3.org/TR/1999/REC-html401-19991224]'
+prev-archive:
+ description: 'Refers to the immediately preceding archive resource.'
+ reference: '[RFC5005]'
+privacy-policy:
+ description: "Refers to a privacy policy associated with the link's context."
+ reference: '[RFC6903], section 4'
+profile:
+ description: "Identifying that a resource representation conforms to a certain profile, without affecting the non-profile semantics of the resource representation."
+ reference: '[RFC6906]'
+ notes: "Profile URIs are primarily intended to be used as identifiers, and thus clients SHOULD NOT indiscriminately access profile URIs."
+related:
+ description: 'Identifies a related resource.'
+ reference: '[RFC4287]'
+replies:
+ description: "Identifies a resource that is a reply to the context of the link."
+ reference: '[RFC4685]'
+search:
+ description: "Refers to a resource that can be used to search through the link's context and related resources."
+ reference: '[http://www.opensearch.org/Specifications/OpenSearch/1.1]'
+section:
+ description: 'Refers to a section in a collection of resources.'
+ reference: '[http://www.w3.org/TR/1999/REC-html401-19991224]'
+self:
+ description: "Conveys an identifier for the link's context."
+ reference: '[RFC4287]'
+service:
+ description: "Indicates a URI that can be used to retrieve a service document."
+ reference: '[RFC5023]'
+ notes: "When used in an Atom document, this relation type specifies Atom Publishing Protocol service documents by default. Requested by James Snell."
+start:
+ description: "Refers to the first resource in a collection of resources."
+ reference: '[http://www.w3.org/TR/1999/REC-html401-19991224]'
+stylesheet:
+ description: 'Refers to a stylesheet.'
+ reference: '[http://www.w3.org/TR/html5/links.html#link-type-stylesheet]'
+subsection:
+ description: "Refers to a resource serving as a subsection in a collection of resources."
+ reference: '[http://www.w3.org/TR/1999/REC-html401-19991224]'
+successor-version:
+ description: "Points to a resource containing the successor version in the version history."
+ reference: '[RFC5829]'
+tag:
+ description: "Gives a tag (identified by the given address) that applies to the current document."
+ reference: '[http://www.w3.org/TR/html5/links.html#link-type-tag]'
+terms-of-service:
+ description: "Refers to the terms of service associated with the link's context."
+ reference: '[RFC6903], section 5'
+timegate:
+ description: 'The Target IRI points to a TimeGate for an Original Resource.'
+ reference: '[RFC7089]'
+ notes: "A TimeGate for an Original Resource is a resource that is capable of datetime negotiation to support access to prior states of the Original Resource."
+timemap:
+ description: 'The Target IRI points to a TimeMap for an Original Resource.'
+ reference: '[RFC7089]'
+ notes: "A TimeMap for an Original Resource is a resource from which a list of URIs of Mementos of the Original Resource is available."
+type:
+ description: "Refers to a resource identifying the abstract semantic type of which the link's context is considered to be an instance."
+ reference: '[RFC6903], section 6'
+up:
+ description: "Refers to a parent document in a hierarchy of documents."
+ reference: '[RFC5988]'
+ notes: "This relation type registration did not indicate a reference. Requested by Noah Slater."
+version-history:
+ description: "Points to a resource containing the version history for the context."
+ reference: '[RFC5829]'
+via:
+ description: "Identifies a resource that is the source of the information in the link's context."
+ reference: '[RFC4287]'
+webmention:
+ description: "Identifies a target URI that supports the Webmention protcol. This allows clients that mention a resource in some form of publishing process to contact that endpoint and inform it that this resource has been mentioned."
+ reference: '[http://www.w3.org/TR/webmention/]'
+ notes: "This is a similar \"Linkback\" mechanism to the ones of Refback, Trackback, and Pingback. It uses a different protocol, though, and thus should be discoverable through its own link relation type."
+working-copy:
+ description: 'Points to a working copy for this resource.'
+ reference: '[RFC5829]'
+working-copy-of:
+ description: "Points to the versioned resource from which this working copy was obtained."
+ reference: '[RFC5829]'
diff --git a/core/core.services.yml b/core/core.services.yml
index 8bce755..24c6cca 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -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']
diff --git a/core/lib/Drupal/Core/Http/LinkRelationType.php b/core/lib/Drupal/Core/Http/LinkRelationType.php
new file mode 100644
index 0000000..b0366e5
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/LinkRelationType.php
@@ -0,0 +1,61 @@
+<?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'] : '';
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/LinkRelationTypeInterface.php b/core/lib/Drupal/Core/Http/LinkRelationTypeInterface.php
new file mode 100644
index 0000000..7faaa04
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/LinkRelationTypeInterface.php
@@ -0,0 +1,88 @@
+<?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();
+
+}
diff --git a/core/lib/Drupal/Core/Http/LinkRelationTypeManager.php b/core/lib/Drupal/Core/Http/LinkRelationTypeManager.php
new file mode 100644
index 0000000..11887aa
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/LinkRelationTypeManager.php
@@ -0,0 +1,63 @@
+<?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;
+ }
+
+}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php
index 355df51..fc9248a 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php
@@ -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 @@ abstract class CommentHalJsonTestBase extends CommentResourceTestBase {
];
}
- /**
- * {@inheritdoc}
- */
- protected function getExpectedCacheContexts() {
- // The 'url.site' cache context is added for '_links' in the response.
- return Cache::mergeTags(parent::getExpectedCacheContexts(), ['url.site']);
- }
-
}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php
index 8529b76..ef3826b 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php
@@ -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 @@ class EntityTestHalJsonAnonTest extends EntityTestResourceTestBase {
];
}
- /**
- * {@inheritdoc}
- */
- protected function getExpectedCacheContexts() {
- // The 'url.site' cache context is added for '_links' in the response.
- return Cache::mergeTags(parent::getExpectedCacheContexts(), ['url.site']);
- }
-
}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
index cbae1b9..e92a3d6 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
@@ -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 @@ class NodeHalJsonAnonTest extends NodeResourceTestBase {
];
}
- /**
- * {@inheritdoc}
- */
- protected function getExpectedCacheContexts() {
- // The 'url.site' cache context is added for '_links' in the response.
- return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['url.site']);
- }
-
}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php
index fa7c8c9..ff952ea 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php
@@ -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 @@ class TermHalJsonAnonTest extends TermResourceTestBase {
];
}
- /**
- * {@inheritdoc}
- */
- protected function getExpectedCacheContexts() {
- // The 'url.site' cache context is added for '_links' in the response.
- return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['url.site']);
- }
-
}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/User/UserHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/User/UserHalJsonAnonTest.php
index 23f609c..7398f28 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/User/UserHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/User/UserHalJsonAnonTest.php
@@ -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 @@ class UserHalJsonAnonTest extends UserResourceTestBase {
];
}
- /**
- * {@inheritdoc}
- */
- protected function getExpectedCacheContexts() {
- // The 'url.site' cache context is added for '_links' in the response.
- return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['url.site']);
- }
-
}
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index a5cb361..a1631bd 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -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 Drupal\rest\ResourceResponse;
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;
@@ -54,6 +57,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.
*
* @param array $configuration
@@ -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 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
$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 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
}
}
+ $this->addLinkHeaders($entity, $response);
+
return $response;
}
@@ -340,4 +356,35 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
}
}
+ /**
+ * 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);
+ }
+ }
+ }
+
}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php
index 94c0b04..89bb9cf 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php
@@ -113,7 +113,7 @@ abstract class BlockResourceTestBase extends EntityResourceTestBase {
*/
protected function getExpectedCacheContexts() {
// @see ::createEntity()
- return [];
+ return ['url.site'];
}
/**
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index e9d9c48..603fb35 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -266,6 +266,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
*/
protected function getExpectedCacheContexts() {
return [
+ 'url.site',
'user.permissions',
];
}
@@ -341,6 +342,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$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 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// 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
diff --git a/core/tests/Drupal/KernelTests/Core/Http/LinkRelationsTest.php b/core/tests/Drupal/KernelTests/Core/Http/LinkRelationsTest.php
new file mode 100644
index 0000000..d01537b
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Http/LinkRelationsTest.php
@@ -0,0 +1,52 @@
+<?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()));
+ }
+
+}