diff --git a/core/lib/Drupal/Core/Entity/Annotation/EntityType.php b/core/lib/Drupal/Core/Entity/Annotation/EntityType.php index 30c82528cce..beaa4e369fb 100644 --- a/core/lib/Drupal/Core/Entity/Annotation/EntityType.php +++ b/core/lib/Drupal/Core/Entity/Annotation/EntityType.php @@ -252,6 +252,41 @@ class EntityType extends Plugin { */ public $menu_path_wildcard; + /** + * Link templates using the URI template syntax. + * + * Links are an array of standard link relations to the URI template that + * should be used for them. Where possible, link relationships should use + * established IANA relationships rather than custom relationships. + * + * Every entity type should, at minimum, define "canonical", which is the + * pattern for URIs to that entity. Even if the entity will have no HTML page + * exposed to users it should still have a canonical URI in order to be + * compatible with web services. Entities that will be user-editable via an + * HTML page must also define an "edit-form" relationship. + * + * By default, the following placeholders are supported: + * - entityType: The machine name of the entity type. + * - bundle: The bundle machine name of the entity. + * - id: The unique ID of the entity. + * - uuid: The UUID of the entity. + * - [entityType]: The entity type itself will also be a valid token for the + * ID of the entity. For instance, a placeholder of {node} used on the Node + * class would have the same value as {id}. This is generally preferred + * over "id" for better self-documentation. + * + * Specific entity types may also expand upon this list by overriding the + * uriPlaceholderReplacements() method. + * + * @link http://www.iana.org/assignments/link-relations/link-relations.xml @endlink + * @link http://tools.ietf.org/html/rfc6570 @endlink + * + * @var array + */ + public $links = array( + 'canonical' => '/entity/{entityType}/{id}', + ); + /** * Specifies whether a module exposing permissions for the current entity type * should use entity-type level granularity, bundle level granularity or just diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index 43f5ab0ce3f..9e95a0b0f4d 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -181,6 +181,19 @@ class Entity implements IteratorAggregate, EntityInterface { return $uri; } + /** + * {@inheritdoc} + * + * Returns a list of URI relationships supported by this entity. + * + * @return array + * An array of link relationships supported by this entity. + */ + public function uriRelationships() { + $entity_info = $this->entityInfo(); + return isset($entity_info['links']) ? array_keys($entity_info['links']) : array(); + } + /** * Implements \Drupal\Core\Entity\EntityInterface::get(). */ diff --git a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php index 9c8205d95a1..475f7b34c60 100644 --- a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php +++ b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php @@ -211,6 +211,13 @@ class EntityBCDecorator implements IteratorAggregate, EntityInterface { $this->decorated = clone $this->decorated; } + /** + * Forwards the call to the decorated entity. + */ + public function uriRelationships() { + return $this->decorated->uriRelationships(); + } + /** * Forwards the call to the decorated entity. */ @@ -355,8 +362,8 @@ class EntityBCDecorator implements IteratorAggregate, EntityInterface { /** * Forwards the call to the decorated entity. */ - public function uri() { - return $this->decorated->uri(); + public function uri($rel = 'canonical') { + return $this->decorated->uri($rel); } /** diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php index 57fe1866521..692c3291dab 100644 --- a/core/lib/Drupal/Core/Entity/EntityInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityInterface.php @@ -135,6 +135,14 @@ interface EntityInterface extends ComplexDataInterface, AccessibleInterface, Tra */ public function uri(); + /** + * Returns a list of URI relationships supported by this entity. + * + * @return array + * An array of link relationships supported by this entity. + */ + public function uriRelationships(); + /** * Saves an entity permanently. * diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index 078094c758f..de0fb46e92c 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -73,6 +73,13 @@ class EntityNG extends Entity { */ protected $fieldDefinitions; + /** + * Local cache for URI placeholder substitution values. + * + * @var array + */ + protected $uriPlaceholderReplacements; + /** * Overrides Entity::__construct(). */ @@ -135,6 +142,61 @@ class EntityNG extends Entity { return $this->get('uuid')->value; } + /** + * {@inheritdoc} + */ + public function uri($rel = 'canonical') { + $entity_info = $this->entityInfo(); + + $link_templates = isset($entity_info['links']) ? $entity_info['links'] : array(); + + if (isset($link_templates[$rel])) { + $template = $link_templates[$rel]; + $replacements = $this->uriPlaceholderReplacements(); + $uri['path'] = str_replace(array_keys($replacements), array_values($replacements), $template); + + // @todo Remove this once http://drupal.org/node/1888424 is in and we can + // move the BC handling of / vs. no-/ to the generator. + $uri['path'] = trim($uri['path'], '/'); + + // Pass the entity data to url() so that alter functions do not need to + // look up this entity again. + $uri['options']['entity_type'] = $this->entityType; + $uri['options']['entity'] = $this; + return $uri; + } + + // For a canonical link (that is, a link to self), look up the stack for + // default logic. Other relationship types are not supported by parent + // classes. + if ($rel == 'canonical') { + return parent::uri(); + } + } + + /** + * Returns an array of placeholders for this entity. + * + * Individual entity classes may override this method to add additional + * placeholders if desired. If so, they should be sure to replicate the + * property caching logic. + * + * @return array + * An array of URI placeholders. + */ + protected function uriPlaceholderReplacements() { + if (empty($this->uriPlaceholderReplacements)) { + $this->uriPlaceholderReplacements = array( + '{entityType}' => $this->entityType(), + '{bundle}' => $this->bundle(), + '{id}' => $this->id(), + '{uuid}' => $this->uuid(), + '{' . $this->entityType() . '}' => $this->id(), + ); + } + return $this->uriPlaceholderReplacements; + } + /** * Implements \Drupal\Core\TypedData\ComplexDataInterface::get(). */ diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 8d9cb4b68f1..7a1ac04103d 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -1568,10 +1568,11 @@ function template_preprocess_comment(&$variables) { } $uri = $comment->uri(); + $permalink_uri = $comment->permalink(); $uri['options'] += array('attributes' => array('class' => 'permalink', 'rel' => 'bookmark')); $variables['title'] = l($comment->subject->value, $uri['path'], $uri['options']); - $variables['permalink'] = l(t('Permalink'), $uri['path'], $uri['options']); + $variables['permalink'] = l(t('Permalink'), $permalink_uri['path'], $permalink_uri['options']); $variables['submitted'] = t('Submitted by !username on !datetime', array('!username' => $variables['author'], '!datetime' => $variables['created'])); if ($comment->pid->target_id) { @@ -1589,10 +1590,10 @@ function template_preprocess_comment(&$variables) { else { $variables['parent_changed'] = format_date($comment_parent->changed->value); } - $uri_parent = $comment_parent->uri(); - $uri_parent['options'] += array('attributes' => array('class' => 'permalink', 'rel' => 'bookmark')); - $variables['parent_title'] = l($comment_parent->subject->value, $uri_parent['path'], $uri_parent['options']); - $variables['parent_permalink'] = l(t('Parent permalink'), $uri_parent['path'], $uri_parent['options']); + $permalink_uri_parent = $comment_parent->permalink(); + $permalink_uri_parent['options'] += array('attributes' => array('class' => array('permalink'), 'rel' => 'bookmark')); + $variables['parent_title'] = l($comment_parent->subject->value, $permalink_uri_parent['path'], $permalink_uri_parent['options']); + $variables['parent_permalink'] = l(t('Parent permalink'), $permalink_uri_parent['path'], $permalink_uri_parent['options']); $variables['parent'] = t('In reply to !parent_title by !parent_username', array('!parent_username' => $variables['parent_author'], '!parent_title' => $variables['parent_title'])); } diff --git a/core/modules/comment/lib/Drupal/comment/CommentInterface.php b/core/modules/comment/lib/Drupal/comment/CommentInterface.php index c6bd7c70cb8..e586087b739 100644 --- a/core/modules/comment/lib/Drupal/comment/CommentInterface.php +++ b/core/modules/comment/lib/Drupal/comment/CommentInterface.php @@ -14,4 +14,13 @@ use Drupal\Core\Entity\ContentEntityInterface; */ interface CommentInterface extends ContentEntityInterface { + /** + * Returns the permalink URL for this comment. + * + * @return array + * An array containing the 'path' and 'options' keys used to build the URI + * of the comment, and matching the signature of + * UrlGenerator::generateFromPath(). + */ + public function permalink(); } diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php index 55a1bd3c109..2f38ea808b0 100644 --- a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php +++ b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php @@ -41,6 +41,10 @@ use Drupal\Core\Language\Language; * "bundle" = "node_type", * "label" = "subject", * "uuid" = "uuid" + * }, + * links = { + * "canonical" = "/comment/{comment}", + * "edit-form" = "/comment/{comment}/edit" * } * ) */ @@ -219,4 +223,15 @@ class Comment extends EntityNG implements CommentInterface { public function id() { return $this->get('cid')->value; } + + /** + * {@inheritdoc} + */ + public function permalink() { + + $url['path'] = 'node/' . $this->nid->value; + $url['options'] = array('fragment' => 'comment-' . $this->id()); + + return $url; + } } diff --git a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php index eb047c1094f..3343da49e05 100644 --- a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php +++ b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php @@ -48,7 +48,12 @@ use Drupal\node\NodeBCDecorator; * "bundle" = "type" * }, * route_base_path = "admin/structure/types/manage/{bundle}", - * permission_granularity = "bundle" + * permission_granularity = "bundle", + * links = { + * "canonical" = "/node/{node}", + * "edit-form" = "/node/{node}/edit", + * "version-history" = "/node/{node}/revisions" + * } * ) */ class Node extends EntityNG implements NodeInterface { diff --git a/core/modules/node/node.module b/core/modules/node/node.module index c297fd90a74..ef9fd7f1c6e 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -2164,11 +2164,18 @@ function node_page_view(EntityInterface $node) { // of the active trail, and the link name becomes the page title. // Thus, we must explicitly set the page title to be the node title. drupal_set_title($node->label()); - $uri = $node->uri(); - // Set the node path as the canonical URL to prevent duplicate content. - drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url($uri['path'], $uri['options'])), TRUE); - // Set the non-aliased path as a default shortlink. - drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE); + + foreach ($node->uriRelationships() as $rel) { + $uri = $node->uri($rel); + // Set the node path as the canonical URL to prevent duplicate content. + drupal_add_html_head_link(array('rel' => $rel, 'href' => url($uri['path'], $uri['options'])), TRUE); + + if ($rel == 'canonical') { + // Set the non-aliased canonical path as a default shortlink. + drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE); + } + } + return node_show($node); } diff --git a/core/modules/rdf/lib/Drupal/rdf/Tests/CommentAttributesTest.php b/core/modules/rdf/lib/Drupal/rdf/Tests/CommentAttributesTest.php index dbdba317940..e08098ed6f9 100644 --- a/core/modules/rdf/lib/Drupal/rdf/Tests/CommentAttributesTest.php +++ b/core/modules/rdf/lib/Drupal/rdf/Tests/CommentAttributesTest.php @@ -135,11 +135,11 @@ class CommentAttributesTest extends CommentTestBase { $this->drupalLogin($this->web_user); $comment_1 = $this->saveComment($this->node->nid, $this->web_user->uid); - $comment_1_uri = url('comment/' . $comment_1->id(), array('fragment' => 'comment-' . $comment_1->id(), 'absolute' => TRUE)); + $comment_1_uri = url('comment/' . $comment_1->id(), array('absolute' => TRUE)); // Posts a reply to the first comment. $comment_2 = $this->saveComment($this->node->nid, $this->web_user->uid, NULL, $comment_1->id()); - $comment_2_uri = url('comment/' . $comment_2->id(), array('fragment' => 'comment-' . $comment_2->id(), 'absolute' => TRUE)); + $comment_2_uri = url('comment/' . $comment_2->id(), array('absolute' => TRUE)); $parser = new \EasyRdf_Parser_Rdfa(); $graph = new \EasyRdf_Graph(); @@ -176,7 +176,8 @@ class CommentAttributesTest extends CommentTestBase { * An array containing information about an anonymous user. */ function _testBasicCommentRdfaMarkup($graph, $comment, $account = array()) { - $comment_uri = url('comment/' . $comment->id(), array('fragment' => 'comment-' . $comment->id(), 'absolute' => TRUE)); + $uri = $comment->uri(); + $comment_uri = url($uri['path'], $uri['options'] + array('absolute' => TRUE)); // Comment type. $expected_value = array( @@ -235,7 +236,12 @@ class CommentAttributesTest extends CommentTestBase { else { // The author is expected to be a blank node. $author_uri = $graph->get($comment_uri, ''); - $this->assertTrue($author_uri->isBnode(), 'Comment relation to author found in RDF output (sioc:has_creator).'); + if ($author_uri instanceof \EasyRdf_Resource) { + $this->assertTrue($author_uri->isBnode(), 'Comment relation to author found in RDF output (sioc:has_creator) and author is blank node.'); + } + else { + $this->fail('Comment relation to author found in RDF output (sioc:has_creator).'); + } } // Author name. diff --git a/core/modules/rdf/rdf.module b/core/modules/rdf/rdf.module index aaea6bbb016..535497a7ab0 100644 --- a/core/modules/rdf/rdf.module +++ b/core/modules/rdf/rdf.module @@ -419,7 +419,7 @@ function rdf_comment_load($comments) { $comment->rdf_data['date'] = rdf_rdfa_attributes($comment->rdf_mapping['created'], $comment->created->value); $comment->rdf_data['nid_uri'] = url('node/' . $comment->nid->target_id); if ($comment->pid->target_id) { - $comment->rdf_data['pid_uri'] = url('comment/' . $comment->pid->target_id, array('fragment' => 'comment-' . $comment->pid->target_id)); + $comment->rdf_data['pid_uri'] = url('comment/' . $comment->pid->target_id); } } } diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php index 28741ec3d39..5f3dc785f3c 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php @@ -43,6 +43,10 @@ use Drupal\taxonomy\TermInterface; * bundle_keys = { * "bundle" = "vid" * }, + * links = { + * "canonical" = "/taxonomy/term/{taxonomy_term}", + * "edit-form" = "/taxonomy/term/{taxonomy_term}/edit" + * }, * menu_base_path = "taxonomy/term/%taxonomy_term", * route_base_path = "admin/structure/taxonomy/manage/{bundle}", * permission_granularity = "bundle" diff --git a/core/modules/taxonomy/taxonomy.pages.inc b/core/modules/taxonomy/taxonomy.pages.inc index 72c1fb19951..4865b3e2f3a 100644 --- a/core/modules/taxonomy/taxonomy.pages.inc +++ b/core/modules/taxonomy/taxonomy.pages.inc @@ -32,12 +32,16 @@ function taxonomy_term_page(Term $term) { drupal_set_breadcrumb($breadcrumb); drupal_add_feed('taxonomy/term/' . $term->id() . '/feed', 'RSS - ' . $term->label()); - $uri = $term->uri(); + foreach ($term->uriRelationships() as $rel) { + $uri = $term->uri($rel); + // Set the term path as the canonical URL to prevent duplicate content. + drupal_add_html_head_link(array('rel' => $rel, 'href' => url($uri['path'], $uri['options'])), TRUE); - // Set the term path as the canonical URL to prevent duplicate content. - drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url($uri['path'], $uri['options'])), TRUE); - // Set the non-aliased path as a default shortlink. - drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE); + if ($rel == 'canonical') { + // Set the non-aliased canonical path as a default shortlink. + drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE); + } + } $build['taxonomy_terms'] = taxonomy_term_view_multiple(array($term->id() => $term)); if ($nids = taxonomy_select_nodes($term->id(), TRUE, config('node.settings')->get('items_per_page'))) { diff --git a/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php b/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php index 8690329c36f..1f55b49b2da 100644 --- a/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php +++ b/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php @@ -41,6 +41,10 @@ use Drupal\Core\Language\Language; * entity_keys = { * "id" = "uid", * "uuid" = "uuid" + * }, + * links = { + * "canonical" = "/user/{user}", + * "edit-form" = "/user/{user}/edit" * } * ) */ diff --git a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php index cbaa672d9c8..cdc3bd74d33 100644 --- a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php +++ b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php @@ -1135,4 +1135,13 @@ class ViewUI implements ViewStorageInterface { public function onChange($property_name) { $this->storage->onChange($property_name); } + + /** + * {@inheritdoc} + */ + public function uriRelationships() { + return $this->storage->uriRelationships(); + } + + }