diff --git a/core/core.services.yml b/core/core.services.yml index 18e37f9..6c4ad0d 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -307,7 +307,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'] diff --git a/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php index 9903fe3..686f0d3 100644 --- a/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php +++ b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php @@ -83,7 +83,7 @@ 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. diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php index ee30e2c..b8fcdeb 100644 --- a/core/lib/Drupal/Core/Url.php +++ b/core/lib/Drupal/Core/Url.php @@ -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; } @@ -291,6 +305,25 @@ public function setOption($name, $value) { } /** + * 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. * * @param bool $absolute @@ -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()); } diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php index 1ebc5bf..f8a2f90 100644 --- a/core/lib/Drupal/Core/Utility/LinkGenerator.php +++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php @@ -35,26 +35,16 @@ class LinkGenerator implements LinkGeneratorInterface { protected $moduleHandler; /** - * The path alias manager. - * - * @var \Drupal\Core\Path\AliasManagerInterface - */ - protected $aliasManager; - - /** * Constructs a LinkGenerator instance. * * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator * 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(); } } diff --git a/core/modules/link/lib/Drupal/link/LinkItemInterface.php b/core/modules/link/lib/Drupal/link/LinkItemInterface.php new file mode 100644 index 0000000..f494e08 --- /dev/null +++ b/core/modules/link/lib/Drupal/link/LinkItemInterface.php @@ -0,0 +1,40 @@ + $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; } } - diff --git a/core/modules/link/lib/Drupal/link/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php b/core/modules/link/lib/Drupal/link/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php index a123e80..7a427e5 100644 --- a/core/modules/link/lib/Drupal/link/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php +++ b/core/modules/link/lib/Drupal/link/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php @@ -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; diff --git a/core/modules/link/lib/Drupal/link/Plugin/Field/FieldType/LinkItem.php b/core/modules/link/lib/Drupal/link/Plugin/Field/FieldType/LinkItem.php index 58a5826..323987f 100644 --- a/core/modules/link/lib/Drupal/link/Plugin/Field/FieldType/LinkItem.php +++ b/core/modules/link/lib/Drupal/link/Plugin/Field/FieldType/LinkItem.php @@ -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); } - } diff --git a/core/modules/link/lib/Drupal/link/Plugin/Field/FieldWidget/LinkWidget.php b/core/modules/link/lib/Drupal/link/Plugin/Field/FieldWidget/LinkWidget.php index 7f69639..80ba9b4 100644 --- a/core/modules/link/lib/Drupal/link/Plugin/Field/FieldWidget/LinkWidget.php +++ b/core/modules/link/lib/Drupal/link/Plugin/Field/FieldWidget/LinkWidget.php @@ -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('', 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' => '', '%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')), ); @@ -82,6 +113,28 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen } /** + * Is the LinkItem field definition configured to support links to routes? + * + * @return bool + * TRUE or FALSE + */ + protected function supportsInternalLinks() { + $link_type = $this->getFieldSetting('link_type'); + return (bool) ($link_type & LinkItemInterface::LINK_INTERNAL); + } + + /** + * Is the LinkItem field definition configured to support external URLs? + * + * @return bool + * TRUE or FALSE + */ + protected function supportsExternalLinks() { + $link_type = $this->getFieldSetting('link_type'); + return (bool) ($link_type & LinkItemInterface::LINK_EXTERNAL); + } + + /** * {@inheritdoc} */ public function settingsForm(array $form, array &$form_state) { @@ -89,15 +142,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'), + '#title' => $this->t('Placeholder for link text'), '#default_value' => $this->getSetting('placeholder_title'), - '#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.'), '#states' => array( 'invisible' => array( ':input[name="instance[settings][title]"]' => array('value' => DRUPAL_DISABLED), @@ -117,31 +170,62 @@ public function settingsSummary() { $placeholder_title = $this->getSetting('placeholder_title'); $placeholder_url = $this->getSetting('placeholder_url'); if (empty($placeholder_title) && empty($placeholder_url)) { - $summary[] = t('No placeholders'); + $summary[] = $this->t('No placeholders'); } else { if (!empty($placeholder_title)) { - $summary[] = t('Title placeholder: @placeholder_title', array('@placeholder_title' => $placeholder_title)); + $summary[] = $this->t('Title placeholder: @placeholder_title', array('@placeholder_title' => $placeholder_title)); } if (!empty($placeholder_url)) { - $summary[] = t('URL placeholder: @placeholder_url', array('@placeholder_url' => $placeholder_url)); + $summary[] = $this->t('URL placeholder: @placeholder_url', array('@placeholder_url' => $placeholder_url)); } } return $summary; } - /** - * Form element validation handler for link_field_widget_form(). + * Form element validation handler; Validates the title property. * * Conditionally requires the link title if a URL value was filled in. */ - function validateTitle(&$element, &$form_state, $form) { + public function validateTitle(&$element, &$form_state, $form) { if ($element['url']['#value'] !== '' && $element['title']['#value'] === '') { $element['title']['#required'] = TRUE; - form_error($element['title'], $form_state, t('!name field is required.', array('!name' => $element['title']['#title']))); + \Drupal::formBuilder()->setError($element['title'], $form_state, $this->t('!name field is required.', array('!name' => $element['title']['#title']))); } } -} + /** + * {@inheritdoc} + */ + public function massageFormValues(array $values, array $form, array &$form_state) { + foreach ($values as &$value) { + if (!empty($value['url'])) { + try { + $parsed_url = UrlHelper::parse($value['url']); + + $url = Url::createFromPath($parsed_url['path']); + $url->setOption('query', $parsed_url['query']); + $url->setOption('fragment', $parsed_url['fragment']); + $url->setOption('attributes', $value['attributes']); + + $value += $url->toArray(); + // Reset the URL value to contain only the path. + $value['url'] = $parsed_url['path']; + } + catch (NotFoundHttpException $e) { + // Nothing to do here, LinkTypeConstraintValidator emits errors. + } + catch (MatchingRouteNotFoundException $e) { + // Nothing to do here, LinkTypeConstraintValidator emits errors. + } + catch (ParamNotConvertedException $e) { + // Nothing to do here, LinkTypeConstraintValidator emits errors. + } + } + } + return $values; + } + +} diff --git a/core/modules/link/lib/Drupal/link/Plugin/Validation/Constraint/LinkTypeConstraint.php b/core/modules/link/lib/Drupal/link/Plugin/Validation/Constraint/LinkTypeConstraint.php new file mode 100644 index 0000000..de28bab --- /dev/null +++ b/core/modules/link/lib/Drupal/link/Plugin/Validation/Constraint/LinkTypeConstraint.php @@ -0,0 +1,93 @@ +context = $context; + } + + /** + * {@inheritdoc} + */ + public function validatedBy() { + return get_class($this); + } + + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) { + if (isset($value)) { + $url_is_valid = TRUE; + /** @var $link_item \Drupal\link\LinkItemInterface */ + $link_item = $value; + $link_type = $link_item->getFieldDefinition()->getSetting('link_type'); + $url_string = $link_item->url; + // Validate the url property. + if ($url_string !== '') { + try { + // @todo This shouldn't be needed, but massageFormValues() may not + // run. + $parsed_url = UrlHelper::parse($url_string); + + $url = Url::createFromPath($parsed_url['path']); + + if ($url->isExternal() && !UrlHelper::isValid($url_string, TRUE)) { + $url_is_valid = FALSE; + } + elseif ($url->isExternal() && !($link_type & LinkItemInterface::LINK_EXTERNAL)) { + $url_is_valid = FALSE; + } + } + catch (NotFoundHttpException $e) { + $url_is_valid = FALSE; + } + catch (MatchingRouteNotFoundException $e) { + $url_is_valid = FALSE; + } + catch (ParamNotConvertedException $e) { + $url_is_valid = FALSE; + } + } + if (!$url_is_valid) { + $this->context->addViolation($this->message, array('%url' => $url_string)); + } + } + } +} + diff --git a/core/modules/link/lib/Drupal/link/Tests/LinkFieldTest.php b/core/modules/link/lib/Drupal/link/Tests/LinkFieldTest.php index 12fc40a..e335fd7 100644 --- a/core/modules/link/lib/Drupal/link/Tests/LinkFieldTest.php +++ b/core/modules/link/lib/Drupal/link/Tests/LinkFieldTest.php @@ -7,8 +7,9 @@ namespace Drupal\link\Tests; -use Drupal\simpletest\WebTestBase; use Drupal\Component\Utility\String; +use Drupal\link\LinkItemInterface; +use Drupal\simpletest\WebTestBase; /** * Tests link field widgets and formatters. @@ -73,14 +74,16 @@ function testURLValidation() { 'type' => 'link', )); $this->field->save(); - entity_create('field_instance_config', array( + $this->instance = entity_create('field_instance_config', array( 'field_name' => $field_name, 'entity_type' => 'entity_test', 'bundle' => 'entity_test', 'settings' => array( 'title' => DRUPAL_DISABLED, + 'link_type' => LinkItemInterface::LINK_GENERIC, ), - ))->save(); + )); + $this->instance->save(); entity_get_form_display('entity_test', 'entity_test', 'default') ->setComponent($field_name, array( 'type' => 'link_default', @@ -100,21 +103,16 @@ function testURLValidation() { $this->assertFieldByName("{$field_name}[0][url]", '', 'Link URL field is displayed'); $this->assertRaw('placeholder="http://example.com"'); - // Verify that a valid URL can be submitted. - $value = 'http://www.example.com/'; - $edit = array( - 'user_id' => 1, - 'name' => $this->randomName(), - "{$field_name}[0][url]" => $value, + // Define some valid URLs. + $valid_external_entries = array( + 'http://www.example.com/', + ); + $valid_internal_entries = array( + 'entity_test/add', ); - $this->drupalPostForm(NULL, $edit, t('Save')); - preg_match('|entity_test/manage/(\d+)|', $this->url, $match); - $id = $match[1]; - $this->assertText(t('entity_test @id has been created.', array('@id' => $id))); - $this->assertRaw($value); - // Verify that invalid URLs cannot be submitted. - $wrong_entries = array( + // Define some invalid URLs. + $invalid_external_entries = array( // Missing protcol 'not-an-url', // Invalid protocol @@ -122,14 +120,66 @@ function testURLValidation() { // Missing host name 'http://', ); - $this->drupalGet('entity_test/add'); - foreach ($wrong_entries as $invalid_value) { + $invalid_internal_entries = array( + 'non/existing/path', + ); + + // Test external and internal URLs for 'link_type' = LinkItemInterface::LINK_GENERIC. + $this->assertValidEntries($field_name, $valid_external_entries + $valid_internal_entries); + $this->assertInvalidEntries($field_name, $invalid_external_entries + $invalid_internal_entries); + + // Test external URLs for 'link_type' = LinkItemInterface::LINK_EXTERNAL. + $this->instance->settings['link_type'] = LinkItemInterface::LINK_EXTERNAL; + $this->instance->save(); + $this->assertValidEntries($field_name, $valid_external_entries); + $this->assertInvalidEntries($field_name, $valid_internal_entries + $invalid_external_entries); + + // Test external URLs for 'link_type' = LinkItemInterface::LINK_INTERNAL. + $this->instance->settings['link_type'] = LinkItemInterface::LINK_INTERNAL; + $this->instance->save(); + $this->assertValidEntries($field_name, $valid_internal_entries); + $this->assertInvalidEntries($field_name, $valid_external_entries + $invalid_internal_entries); + } + + /** + * Asserts that valid URLs can be submitted. + * + * @param string $field_name + * The field name. + * @param array $valid_entries + * An array of valid URL entries. + */ + protected function assertValidEntries($field_name, array $valid_entries) { + foreach ($valid_entries as $value) { + $edit = array( + 'user_id' => 1, + 'name' => $this->randomName(), + "{$field_name}[0][url]" => $value, + ); + $this->drupalPostForm('entity_test/add', $edit, t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->url, $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', array('@id' => $id))); + $this->assertRaw($value); + } + } + + /** + * Asserts that invalid URLs cannot be submitted. + * + * @param string $field_name + * The field name. + * @param array $invalid_entries + * An array of invalid URL entries. + */ + protected function assertInvalidEntries($field_name, array $invalid_entries) { + foreach ($invalid_entries as $invalid_value) { $edit = array( 'user_id' => 1, 'name' => $this->randomName(), "{$field_name}[0][url]" => $invalid_value, ); - $this->drupalPostForm(NULL, $edit, t('Save')); + $this->drupalPostForm('entity_test/add', $edit, t('Save')); $this->assertText(t('The URL @url is not valid.', array('@url' => $invalid_value))); } } @@ -153,6 +203,7 @@ function testLinkTitle() { 'label' => 'Read more about this entity', 'settings' => array( 'title' => DRUPAL_OPTIONAL, + 'link_type' => LinkItemInterface::LINK_GENERIC, ), )); $this->instance->save(); @@ -272,6 +323,7 @@ function testLinkFormatter() { 'bundle' => 'entity_test', 'settings' => array( 'title' => DRUPAL_OPTIONAL, + 'link_type' => LinkItemInterface::LINK_GENERIC, ), ))->save(); entity_get_form_display('entity_test', 'entity_test', 'default') @@ -413,6 +465,7 @@ function testLinkSeparateFormatter() { 'bundle' => 'entity_test', 'settings' => array( 'title' => DRUPAL_OPTIONAL, + 'link_type' => LinkItemInterface::LINK_GENERIC, ), ))->save(); $display_options = array( diff --git a/core/modules/link/lib/Drupal/link/Tests/LinkItemTest.php b/core/modules/link/lib/Drupal/link/Tests/LinkItemTest.php index d1e4ea6..8125790 100644 --- a/core/modules/link/lib/Drupal/link/Tests/LinkItemTest.php +++ b/core/modules/link/lib/Drupal/link/Tests/LinkItemTest.php @@ -7,6 +7,7 @@ namespace Drupal\link\Tests; +use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldItemInterface; use Drupal\field\Tests\FieldUnitTestBase; @@ -53,12 +54,14 @@ public function setUp() { public function testLinkItem() { // Create entity. $entity = entity_create('entity_test'); - $url = 'http://www.drupal.org'; + $url = 'http://www.drupal.org?test_param=test_value'; + $parsed_url = UrlHelper::parse($url); $title = $this->randomName(); $class = $this->randomName(); - $entity->field_test->url = $url; + $entity->field_test->url = $parsed_url['path']; $entity->field_test->title = $title; - $entity->field_test->first()->get('attributes')->set('class', $class); + $entity->field_test->first()->get('options')->set('query', $parsed_url['query']); + $entity->field_test->first()->get('options')->set('attributes', array('class' => $class)); $entity->name->value = $this->randomName(); $entity->save(); @@ -67,11 +70,22 @@ public function testLinkItem() { $entity = entity_load('entity_test', $id); $this->assertTrue($entity->field_test instanceof FieldItemListInterface, 'Field implements interface.'); $this->assertTrue($entity->field_test[0] instanceof FieldItemInterface, 'Field item implements interface.'); - $this->assertEqual($entity->field_test->url, $url); - $this->assertEqual($entity->field_test[0]->url, $url); + $this->assertEqual($entity->field_test->url, $parsed_url['path']); + $this->assertEqual($entity->field_test[0]->url, $parsed_url['path']); $this->assertEqual($entity->field_test->title, $title); $this->assertEqual($entity->field_test[0]->title, $title); - $this->assertEqual($entity->field_test->attributes['class'], $class); + $this->assertEqual($entity->field_test->options['attributes']['class'], $class); + $this->assertEqual($entity->field_test->options['query'], $parsed_url['query']); + + // Update only the entity name property to check if the link field data will + // remain intact. + $entity->name->value = $this->randomName(); + $entity->save(); + $id = $entity->id(); + $entity = entity_load('entity_test', $id); + $this->assertEqual($entity->field_test->url, $parsed_url['path']); + $this->assertEqual($entity->field_test->options['attributes']['class'], $class); + $this->assertEqual($entity->field_test->options['query'], $parsed_url['query']); // Verify changing the field value. $new_url = 'http://drupal.org'; @@ -79,17 +93,19 @@ public function testLinkItem() { $new_class = $this->randomName(); $entity->field_test->url = $new_url; $entity->field_test->title = $new_title; - $entity->field_test->first()->get('attributes')->set('class', $new_class); + $entity->field_test->first()->get('options')->set('query', NULL); + $entity->field_test->first()->get('options')->set('attributes', array('class' => $new_class)); $this->assertEqual($entity->field_test->url, $new_url); $this->assertEqual($entity->field_test->title, $new_title); - $this->assertEqual($entity->field_test->attributes['class'], $new_class); + $this->assertEqual($entity->field_test->options['attributes']['class'], $new_class); + $this->assertNull($entity->field_test->options['query']); // Read changed entity and assert changed values. $entity->save(); $entity = entity_load('entity_test', $id); $this->assertEqual($entity->field_test->url, $new_url); $this->assertEqual($entity->field_test->title, $new_title); - $this->assertEqual($entity->field_test->attributes['class'], $new_class); + $this->assertEqual($entity->field_test->options['attributes']['class'], $new_class); } } diff --git a/core/modules/link/link.module b/core/modules/link/link.module index 0e830df..2c06c5d 100644 --- a/core/modules/link/link.module +++ b/core/modules/link/link.module @@ -15,7 +15,7 @@ function link_help($path, $arg) { case 'admin/help#link': $output = ''; $output .= '

' . t('About') . '

'; - $output .= '

' . t('The Link module allows you to create fields that contain external URLs and optional link text. See the Field module help and the Field UI help pages for general information on fields and how to create and manage them. For more information, see the online documentation for the Link module.', array('!field' => \Drupal::url('help.page', array('name' => 'field')), '!field_ui' => \Drupal::url('help.page', array('name' => 'field_ui')), '!link_documentation' => 'https://drupal.org/documentation/modules/link')) . '

'; + $output .= '

' . t('The Link module allows you to create fields that contain internal or external URLs and optional link text. See the Field module help and the Field UI help pages for general information on fields and how to create and manage them. For more information, see the online documentation for the Link module.', array('!field' => \Drupal::url('help.page', array('name' => 'field')), '!field_ui' => \Drupal::url('help.page', array('name' => 'field_ui')), '!link_documentation' => 'https://drupal.org/documentation/modules/link')) . '

'; $output .= '

' . t('Uses') . '

'; $output .= '
'; $output .= '
' . t('Managing and displaying link fields') . '
'; @@ -27,7 +27,7 @@ function link_help($path, $arg) { $output .= '
' . t('Adding attributes to links') . '
'; $output .= '
' . t('You can add attributes to links, by changing the Format settings in the Manage display page. Adding rel="nofollow" notifies search engines that links should not be followed.') . '
'; $output .= '
' . t('Validating URLs') . '
'; - $output .= '
' . t('All links are validated after a link field is filled in. They can include anchors or query strings. Links need to start with either the scheme name http or https; other scheme names (for example ftp or git) are not supported.') . '
'; + $output .= '
' . t('All links are validated after a link field is filled in. They can include anchors or query strings.') . '
'; $output .= '
'; return $output; } @@ -39,7 +39,7 @@ function link_help($path, $arg) { function link_theme() { return array( 'link_formatter_link_separate' => array( - 'variables' => array('title' => NULL, 'url_title' => NULL, 'href' => NULL, 'options' => array()), + 'variables' => array('title' => NULL, 'url_title' => NULL, 'url' => NULL), 'template' => 'link-formatter-link-separate', ), ); @@ -57,12 +57,17 @@ function link_theme() { * - title: (optional) A descriptive or alternate title for the link, which * may be different than the actual link text. * - url_title: The anchor text for the link. - * - href: The link URL. - * - options: (optional) An array of options to pass to l(). + * - url: A \Drupal\Core\Url object. */ function template_preprocess_link_formatter_link_separate(&$variables) { if (!empty($variables['title'])) { $variables['title'] = String::checkPlain($variables['title']); } - $variables['link'] = l($variables['url_title'], $variables['href'], $variables['options']); + + if (!$variables['url']->isExternal()) { + $variables['link'] = \Drupal::linkGenerator()->generateFromUrl($variables['url_title'], $variables['url']); + } + else { + $variables['link'] = l($variables['url_title'], $variables['url']->getPath(), $variables['url']->getOptions()); + } } diff --git a/core/tests/Drupal/Tests/Core/ExternalUrlTest.php b/core/tests/Drupal/Tests/Core/ExternalUrlTest.php index ac867c3..37f1a40 100644 --- a/core/tests/Drupal/Tests/Core/ExternalUrlTest.php +++ b/core/tests/Drupal/Tests/Core/ExternalUrlTest.php @@ -142,8 +142,7 @@ public function testToString(Url $url) { */ public function testToArray(Url $url) { $expected = array( - 'route_name' => '', - 'route_parameters' => array(), + 'path' => $this->path, 'options' => array(), ); $this->assertSame($expected, $url->toArray()); @@ -154,10 +153,12 @@ public function testToArray(Url $url) { * * @depends testCreateFromPath * + * @expectedException \UnexpectedValueException + * * @covers ::getRouteName() */ public function testGetRouteName(Url $url) { - $this->assertSame('', $url->getRouteName()); + $url->getRouteName(); } /** @@ -165,10 +166,12 @@ public function testGetRouteName(Url $url) { * * @depends testCreateFromPath * + * @expectedException \UnexpectedValueException + * * @covers ::getRouteParameters() */ public function testGetRouteParameters(Url $url) { - $this->assertSame(array(), $url->getRouteParameters()); + $url->getRouteParameters(); } /** @@ -185,6 +188,17 @@ public function testGetInternalPath(Url $url) { } /** + * Tests the getPath() method. + * + * @depends testCreateFromPath + * + * @covers ::getPath() + */ + public function testGetPath(Url $url) { + $this->assertNotNull($url->getPath()); + } + + /** * Tests the getOptions() method. * * @depends testCreateFromPath diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php index dd786dc..a2a7ec0 100644 --- a/core/tests/Drupal/Tests/Core/UrlTest.php +++ b/core/tests/Drupal/Tests/Core/UrlTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core; +use Drupal\Component\Utility\UrlHelper; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Url; use Drupal\Tests\UnitTestCase; @@ -202,6 +203,31 @@ public function testIsExternal($urls) { } /** + * Tests the getPath() method for internal URLs. + * + * @depends testCreateFromPath + * + * @expectedException \UnexpectedValueException + * + * @covers ::getPath() + */ + public function testGetPathForInternalUrl($urls) { + foreach ($urls as $url) { + $url->getPath(); + } + } + + /** + * Tests the getPath() method for external URLs. + * + * @covers ::getPath + */ + public function testGetPathForExternalUrl() { + $url = Url::createFromPath('http://example.com/test'); + $this->assertEquals('http://example.com/test', $url->getPath()); + } + + /** * Tests the toString() method. * * @param \Drupal\Core\Url[] $urls @@ -256,6 +282,17 @@ public function testGetRouteName($urls) { } /** + * Tests the getRouteName() with an external URL. + * + * @covers ::getRouteName + * @expectedException \UnexpectedValueException + */ + public function testGetRouteNameWithExternalUrl() { + $url = Url::createFromPath('http://example.com'); + $url->getRouteName(); + } + + /** * Tests the getRouteParameters() method. * * @param \Drupal\Core\Url[] $urls @@ -272,6 +309,17 @@ public function testGetRouteParameters($urls) { } /** + * Tests the getRouteParameter() with an external URL. + * + * @covers ::getRouteParameter + * @expectedException \UnexpectedValueException + */ + public function testGetRouteParametersWithExternalUrl() { + $url = Url::createFromPath('http://example.com'); + $url->getRouteParameters(); + } + + /** * Tests the getOptions() method. * * @param \Drupal\Core\Url[] $urls diff --git a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php index 07edbc7..55f4339 100644 --- a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php +++ b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php @@ -43,13 +43,6 @@ class LinkGeneratorTest extends UnitTestCase { protected $moduleHandler; /** - * The mocked path alias manager. - * - * @var \Drupal\Core\Path\AliasManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $aliasManager; - - /** * Contains the LinkGenerator default options. */ protected $defaultOptions = array( @@ -80,9 +73,8 @@ protected function setUp() { $this->urlGenerator = $this->getMock('\Drupal\Core\Routing\UrlGenerator', array(), array(), '', FALSE); $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); - $this->aliasManager = $this->getMock('\Drupal\Core\Path\AliasManagerInterface'); - $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler, $this->aliasManager); + $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler); } /** @@ -344,14 +336,6 @@ public function testGenerateActive() { array('test_route_4', array('object' => '1'), 'test-route-4/1'), ))); - $this->aliasManager->expects($this->exactly(7)) - ->method('getSystemPath') - ->will($this->returnValueMap(array( - array('test-route-1', NULL, 'test-route-1'), - array('test-route-3', NULL, 'test-route-3'), - array('test-route-4/1', NULL, 'test-route-4/1'), - ))); - $this->moduleHandler->expects($this->exactly(8)) ->method('alter');