Commit bf4a5828 authored by Dries's avatar Dries

Issue #2054011 by amateescu, pwolanin, dawehner, YesCT, tim.plunkett,...

Issue #2054011 by amateescu, pwolanin, dawehner, YesCT, tim.plunkett, blueminds, Berdir, das-peter, effulgentsia: Implement built-in support for internal URLs.
parent 4e5b0480
......@@ -309,7 +309,7 @@ services:
- [setContext, ['@?router.request_context']]
link_generator:
class: Drupal\Core\Utility\LinkGenerator
arguments: ['@url_generator', '@module_handler', '@path.alias_manager.cached']
arguments: ['@url_generator', '@module_handler']
router.dynamic:
class: Symfony\Cmf\Component\Routing\DynamicRouter
arguments: ['@router.request_context', '@router.matcher', '@url_generator']
......
......@@ -83,7 +83,7 @@ interface UrlGeneratorInterface extends VersatileGeneratorInterface {
public function generateFromPath($path = NULL, $options = array());
/**
* Gets the internal path of a route.
* Gets the internal path (system path) of a route.
*
* @param string $name
* The route name.
......
......@@ -169,7 +169,7 @@ protected function setExternal() {
$this->path = $this->routeName;
// Set empty route name and parameters.
$this->routeName = '';
$this->routeName = NULL;
$this->routeParameters = array();
return $this;
......@@ -188,8 +188,15 @@ public function isExternal() {
* Returns the route name.
*
* @return string
*
* @throws \UnexpectedValueException.
* If this is an external URL with no corresponding route.
*/
public function getRouteName() {
if ($this->isExternal()) {
throw new \UnexpectedValueException('External URLs do not have an internal route name.');
}
return $this->routeName;
}
......@@ -197,8 +204,15 @@ public function getRouteName() {
* Returns the route parameters.
*
* @return array
*
* @throws \UnexpectedValueException.
* If this is an external URL with no corresponding route.
*/
public function getRouteParameters() {
if ($this->isExternal()) {
throw new \UnexpectedValueException('External URLs do not have internal route parameters.');
}
return $this->routeParameters;
}
......@@ -290,6 +304,25 @@ public function setOption($name, $value) {
return $this;
}
/**
* Returns the external path of the URL.
*
* Only to be used if self::$external is TRUE.
*
* @return string
* The external path.
*
* @throws \UnexpectedValueException
* Thrown when the path was requested for an internal URL.
*/
public function getPath() {
if (!$this->isExternal()) {
throw new \UnexpectedValueException('Internal URLs do not have external paths.');
}
return $this->path;
}
/**
* Sets the absolute value for this Url.
*
......@@ -308,7 +341,7 @@ public function setAbsolute($absolute = TRUE) {
*/
public function toString() {
if ($this->isExternal()) {
return $this->urlGenerator()->generateFromPath($this->path, $this->getOptions());
return $this->urlGenerator()->generateFromPath($this->getPath(), $this->getOptions());
}
return $this->urlGenerator()->generateFromRoute($this->getRouteName(), $this->getRouteParameters(), $this->getOptions());
......@@ -321,11 +354,19 @@ public function toString() {
* An associative array containing all the properties of the route.
*/
public function toArray() {
return array(
'route_name' => $this->getRouteName(),
'route_parameters' => $this->getRouteParameters(),
'options' => $this->getOptions(),
);
if ($this->isExternal()) {
return array(
'path' => $this->getPath(),
'options' => $this->getOptions(),
);
}
else {
return array(
'route_name' => $this->getRouteName(),
'route_parameters' => $this->getRouteParameters(),
'options' => $this->getOptions(),
);
}
}
/**
......@@ -335,24 +376,38 @@ public function toArray() {
* An associative array suitable for a render array.
*/
public function toRenderArray() {
return array(
'#route_name' => $this->getRouteName(),
'#route_parameters' => $this->getRouteParameters(),
'#options' => $this->getOptions(),
);
if ($this->isExternal()) {
return array(
'#href' => $this->getPath(),
'#options' => $this->getOptions(),
);
}
else {
return array(
'#route_name' => $this->getRouteName(),
'#route_parameters' => $this->getRouteParameters(),
'#options' => $this->getOptions(),
);
}
}
/**
* Returns the internal path for this route.
* Returns the internal path (system path) for this route.
*
* This path will not include any prefixes, fragments, or query strings.
*
* @return string
* The internal path for this route.
*
* @throws \UnexpectedValueException.
* If this is an external URL with no corresponding system path.
*
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* System paths should not be used - use route names and parameters.
*/
public function getInternalPath() {
if ($this->isExternal()) {
throw new \Exception('External URLs do not have internal representations.');
throw new \UnexpectedValueException('External URLs do not have internal representations.');
}
return $this->urlGenerator()->getPathFromRoute($this->getRouteName(), $this->getRouteParameters());
}
......
......@@ -34,13 +34,6 @@ class LinkGenerator implements LinkGeneratorInterface {
*/
protected $moduleHandler;
/**
* The path alias manager.
*
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* Constructs a LinkGenerator instance.
*
......@@ -48,13 +41,10 @@ class LinkGenerator implements LinkGeneratorInterface {
* The url generator.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
* The path alias manager.
*/
public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, AliasManagerInterface $alias_manager) {
public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler) {
$this->urlGenerator = $url_generator;
$this->moduleHandler = $module_handler;
$this->aliasManager = $alias_manager;
}
/**
......@@ -107,8 +97,8 @@ public function generateFromUrl($text, Url $url) {
// Add a "data-drupal-link-system-path" attribute to let the
// drupal.active-link library know the path in a standardized manner.
if (!isset($variables['options']['attributes']['data-drupal-link-system-path'])) {
$path = $url->getInternalPath();
$variables['options']['attributes']['data-drupal-link-system-path'] = $this->aliasManager->getSystemPath($path);
// @todo System path is deprecated - use the route name and parameters.
$variables['options']['attributes']['data-drupal-link-system-path'] = $url->getInternalPath();
}
}
......
<?php
/**
* @file
* Contains \Drupal\link\LinkItemInterface.
*/
namespace Drupal\link;
use Drupal\Core\Field\FieldItemInterface;
/**
* Defines an interface for the link field item.
*/
interface LinkItemInterface extends FieldItemInterface {
/**
* Specifies whether the field supports only internal URLs.
*/
const LINK_INTERNAL = 0x01;
/**
* Specifies whether the field supports only external URLs.
*/
const LINK_EXTERNAL = 0x10;
/**
* Specifies whether the field supports both internal and external URLs.
*/
const LINK_GENERIC = 0x11;
/**
* Determines if a link is external.
*
* @return bool
* TRUE if the link is external, FALSE otherwise.
*/
public function isExternal();
}
......@@ -7,11 +7,11 @@
namespace Drupal\link\Plugin\Field\FieldFormatter;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\Utility\String;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Url;
use Drupal\link\LinkItemInterface;
/**
* Plugin implementation of the 'link' formatter.
......@@ -128,7 +128,8 @@ public function viewElements(FieldItemListInterface $items) {
foreach ($items as $delta => $item) {
// By default use the full URL as the link text.
$link_title = $item->url;
$url = $this->buildUrl($item);
$link_title = $url->toString();
// If the title field value is available, use it for the link text.
if (empty($settings['url_only']) && !empty($item->title)) {
......@@ -148,13 +149,18 @@ public function viewElements(FieldItemListInterface $items) {
);
}
else {
$link = $this->buildLink($item);
$element[$delta] = array(
'#type' => 'link',
'#title' => $link_title,
'#href' => $link['path'],
'#options' => $link['options'],
'#options' => $url->getOptions(),
);
if ($url->isExternal()) {
$element[$delta]['#href'] = $url->getPath();
}
else {
$element[$delta]['#route_name'] = $url->getRouteName();
$element[$delta]['#route_parameters'] = $url->getRouteParameters();
}
}
}
......@@ -162,41 +168,36 @@ public function viewElements(FieldItemListInterface $items) {
}
/**
* Builds the link information for a link field item.
* Builds the \Drupal\Core\Url object for a link field item.
*
* @param \Drupal\Core\Field\FieldItemInterface $item
* @param \Drupal\link\LinkItemInterface $item
* The link field item being rendered.
*
* @return array
* An array with the following key/value pairs:
* - 'path': a string suitable for the $path parameter in l().
* - 'options': an array suitable for the $options parameter in l().
* @return \Drupal\Core\Url
* An Url object.
*/
protected function buildLink(FieldItemInterface $item) {
protected function buildUrl(LinkItemInterface $item) {
$settings = $this->getSettings();
// Split out the link into the parts required for url(): path and options.
$parsed_url = UrlHelper::parse($item->url);
$result = array(
'path' => $parsed_url['path'],
'options' => array(
'query' => $parsed_url['query'],
'fragment' => $parsed_url['fragment'],
'attributes' => $item->attributes,
),
);
$options = $item->options;
// Add optional 'rel' attribute to link options.
if (!empty($settings['rel'])) {
$result['options']['attributes']['rel'] = $settings['rel'];
$options['attributes']['rel'] = $settings['rel'];
}
// Add optional 'target' attribute to link options.
if (!empty($settings['target'])) {
$result['options']['attributes']['target'] = $settings['target'];
$options['attributes']['target'] = $settings['target'];
}
if ($item->isExternal()) {
$url = Url::createFromPath($item->url);
$url->setOptions($options);
}
else {
$url = new Url($item->route_name, (array) $item->route_parameters, (array) $options);
}
return $result;
return $url;
}
}
......@@ -48,7 +48,8 @@ public function viewElements(FieldItemListInterface $items) {
foreach ($items as $delta => $item) {
// By default use the full URL as the link text.
$link_title = $item->url;
$url = $this->buildUrl($item);
$link_title = $url->toString();
// If the link text field value is available, use it for the text.
if (empty($settings['url_only']) && !empty($item->title)) {
......@@ -64,19 +65,17 @@ public function viewElements(FieldItemListInterface $items) {
if (empty($item->title)) {
$link_title = NULL;
}
$url_title = $item->url;
$url_title = $url->toString();
if (!empty($settings['trim_length'])) {
$link_title = truncate_utf8($link_title, $settings['trim_length'], FALSE, TRUE);
$url_title = truncate_utf8($item->url, $settings['trim_length'], FALSE, TRUE);
$url_title = truncate_utf8($url_title, $settings['trim_length'], FALSE, TRUE);
}
$link = $this->buildLink($item);
$element[$delta] = array(
'#theme' => 'link_formatter_link_separate',
'#title' => $link_title,
'#url_title' => $url_title,
'#href' => $link['path'],
'#options' => $link['options'],
'#url' => $url,
);
}
return $element;
......
......@@ -11,6 +11,7 @@
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\MapDataDefinition;
use Drupal\link\LinkItemInterface;
/**
* Plugin implementation of the 'link' field type.
......@@ -20,17 +21,19 @@
* label = @Translation("Link"),
* description = @Translation("Stores a URL string, optional varchar link text, and optional blob of attributes to assemble a link."),
* default_widget = "link_default",
* default_formatter = "link"
* default_formatter = "link",
* constraints = {"LinkType" = {}}
* )
*/
class LinkItem extends FieldItemBase {
class LinkItem extends FieldItemBase implements LinkItemInterface {
/**
* {@inheritdoc}
*/
public static function defaultInstanceSettings() {
return array(
'title' => 1,
'title' => DRUPAL_OPTIONAL,
'link_type' => LinkItemInterface::LINK_GENERIC
) + parent::defaultInstanceSettings();
}
......@@ -38,14 +41,20 @@ public static function defaultInstanceSettings() {
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldDefinitionInterface $field_definition) {
$properties['url'] = DataDefinition::create('uri')
$properties['url'] = DataDefinition::create('string')
->setLabel(t('URL'));
$properties['title'] = DataDefinition::create('string')
->setLabel(t('Link text'));
$properties['attributes'] = MapDataDefinition::create()
->setLabel(t('Attributes'));
$properties['route_name'] = DataDefinition::create('string')
->setLabel(t('Route name'));
$properties['route_parameters'] = MapDataDefinition::create()
->setLabel(t('Route parameters'));
$properties['options'] = MapDataDefinition::create()
->setLabel(t('Options'));
return $properties;
}
......@@ -68,8 +77,21 @@ public static function schema(FieldDefinitionInterface $field_definition) {
'length' => 255,
'not null' => FALSE,
),
'attributes' => array(
'description' => 'Serialized array of attributes for the link.',
'route_name' => array(
'description' => 'The machine name of a defined Route this link represents.',
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
'route_parameters' => array(
'description' => 'Serialized array of route parameters of the link.',
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'serialize' => TRUE,
),
'options' => array(
'description' => 'Serialized array of options for the link.',
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
......@@ -85,6 +107,17 @@ public static function schema(FieldDefinitionInterface $field_definition) {
public function instanceSettingsForm(array $form, array &$form_state) {
$element = array();
$element['link_type'] = array(
'#type' => 'radios',
'#title' => t('Allowed link type'),
'#default_value' => $this->getSetting('link_type'),
'#options' => array(
static::LINK_INTERNAL => t('Internal links only'),
static::LINK_EXTERNAL => t('External links only'),
static::LINK_GENERIC => t('Both internal and external links'),
),
);
$element['title'] = array(
'#type' => 'radios',
'#title' => t('Allow link text'),
......@@ -102,18 +135,16 @@ public function instanceSettingsForm(array $form, array &$form_state) {
/**
* {@inheritdoc}
*/
public function preSave() {
// Trim any spaces around the URL and link text.
$this->url = trim($this->url);
$this->title = trim($this->title);
public function isEmpty() {
$value = $this->get('url')->getValue();
return $value === NULL || $value === '';
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
$value = $this->get('url')->getValue();
return $value === NULL || $value === '';
public function isExternal() {
// External links don't have a route_name value.
return empty($this->route_name);
}
}
......@@ -7,8 +7,14 @@
namespace Drupal\link\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Routing\MatchingRouteNotFoundException;
use Drupal\Core\Url;
use Drupal\link\LinkItemInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Plugin implementation of the 'link' widget.
......@@ -37,17 +43,42 @@ public static function defaultSettings() {
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, array &$form_state) {
$default_url_value = NULL;
if (isset($items[$delta]->url)) {
$url = Url::createFromPath($items[$delta]->url);
$url->setOptions($items[$delta]->options);
$default_url_value = ltrim($url->toString(), '/');
}
$element['url'] = array(
'#type' => 'url',
'#title' => t('URL'),
'#title' => $this->t('URL'),
'#placeholder' => $this->getSetting('placeholder_url'),
'#default_value' => isset($items[$delta]->url) ? $items[$delta]->url : NULL,
'#default_value' => $default_url_value,
'#maxlength' => 2048,
'#required' => $element['#required'],
);
// If the field is configured to support internal links, it cannot use the
// 'url' form element and we have to do the validation ourselves.
if ($this->supportsInternalLinks()) {
$element['url']['#type'] = 'textfield';
}
// If the field is configured to allow only internal links, add a useful
// element prefix.
if (!$this->supportsExternalLinks()) {
$element['url']['#field_prefix'] = \Drupal::url('<front>', array(), array('absolute' => TRUE));
}
// If the field is configured to allow both internal and external links,
// show a useful description.
elseif ($this->supportsExternalLinks() && $this->supportsInternalLinks()) {
$element['url']['#description'] = $this->t('This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '<front>', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org'));
}
$element['title'] = array(
'#type' => 'textfield',
'#title' => t('Link text'),
'#title' => $this->t('Link text'),
'#placeholder' => $this->getSetting('placeholder_title'),
'#default_value' => isset($items[$delta]->title) ? $items[$delta]->title : NULL,
'#maxlength' => 255,
......@@ -58,7 +89,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
// settings cannot be saved otherwise.
$is_field_edit_form = ($element['#entity'] === NULL);
if (!$is_field_edit_form && $this->getFieldSetting('title') == DRUPAL_REQUIRED) {
$element['#element_validate'] = array(array($this, 'validateTitle'));
$element['#element_validate'][] = array($this, 'validateTitle');
}
// Exposing the attributes array in the widget is left for alternate and more
......@@ -66,7 +97,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
$element['attributes'] = array(
'#type' => 'value',
'#tree' => TRUE,
'#value' => !empty($items[$delta]->attributes) ? $items[$delta]->attributes : array(),
'#value' => !empty($items[$delta]->options['attributes']) ? $items[$delta]->options['attributes'] : array(),
'#attributes' => array('class' => array('link-field-widget-attributes')),
);
......@@ -81,6 +112,30 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
return $element;
}
/**
* Indicates enabled support for link to routes.
*
* @return bool
* Returns TRUE if the LinkItem field is configured to support links to
* routes, otherwise FALSE.
*/
protected function supportsInternalLinks() {
$link_type = $this->getFieldSetting('link_type');
return (bool) ($link_type & LinkItemInterface::LINK_INTERNAL);
}
/**
* Indicates enabled support for link to external URLs.
*
* @return bool
* Returns TRUE if the LinkItem field is configured to support links to
* external URLs, otherwise FALSE.
*/
protected function supportsExternalLinks() {
$link_type = $this->getFieldSetting('link_type');
return (bool) ($link_type & LinkItemInterface::LINK_EXTERNAL);
}
/**
* {@inheritdoc}
*/
......@@ -89,15 +144,15 @@ public function settingsForm(array $form, array &$form_state) {
$elements['placeholder_url'] = array(
'#type' => 'textfield',
'#title' => t('Placeholder for URL'),
'#title' => $this->t('Placeholder for URL'),
'#default_value' => $this->getSetting('placeholder_url'),
'#description' => t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'),
'#description' => $this->t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'),
);
$elements['placeholder_title'] = array(
'#type' => 'textfield',
'#title' => t('Placeholder for link text'),