diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php index 23dbeaf..aea0bb5 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; @@ -322,9 +322,9 @@ public function toString() { */ public function toArray() { return array( - 'route_name' => $this->getRouteName(), - 'route_parameters' => $this->getRouteParameters(), - 'options' => $this->getOptions(), + 'route_name' => $this->routeName, + 'route_parameters' => $this->routeParameters, + 'options' => $this->options, ); } @@ -335,11 +335,19 @@ 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->external) { + return array( + '#href' => $this->path, + '#options' => $this->options, + ); + } + else { + return array( + '#route_name' => $this->routeName, + '#route_parameters' => $this->routeParameters, + '#options' => $this->options, + ); + } } /** 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..bd71b41 --- /dev/null +++ b/core/modules/link/lib/Drupal/link/LinkItemInterface.php @@ -0,0 +1,24 @@ +buildLink($item); $element[$delta] = array( '#type' => 'link', '#title' => $link_title, - '#href' => $link['path'], - '#options' => $link['options'], - ); + ) + $this->buildUrl($item)->toRenderArray(); } } @@ -155,41 +152,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 = Url::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']; } - return $result; + if ($item->isExternal()) { + $url = Url::createFromPath($item->url); + $url->setOptions($options); + } + else { + $url = new Url($item->route_name, $item->route_paramaters, $options); + } + + 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 4392123..f3417a8 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 @@ -64,13 +64,12 @@ public function viewElements(FieldItemListInterface $items) { $url_title = truncate_utf8($item->url, $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' => $this->buildUrl($item), ); } 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 bc28594..b9e9aad 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 @@ -7,9 +7,11 @@ namespace Drupal\link\Plugin\Field\FieldType; +use Drupal\Component\Utility\Url as UrlHelper; use Drupal\Core\Field\ConfigFieldItemBase; use Drupal\Core\TypedData\DataDefinition; use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\link\LinkItemInterface; /** * Plugin implementation of the 'link' field type. @@ -17,15 +19,31 @@ * @FieldType( * id = "link", * label = @Translation("Link"), - * description = @Translation("Stores a URL string, optional varchar link text, and optional blob of attributes to assemble a link."), + * description = @Translation("Stores a URL string, optional varchar link text, and optional blob of options to assemble a link."), * instance_settings = { + * "url_type" = "2", * "title" = "1" * }, * default_widget = "link_default", * default_formatter = "link" * ) */ -class LinkItem extends ConfigFieldItemBase { +class LinkItem extends ConfigFieldItemBase implements LinkItemInterface { + + /** + * Specifies whether the field supports only internal URLs. + */ + const LINK_INTERNAL = 0; + + /** + * Specifies whether the field supports only external URLs. + */ + const LINK_EXTERNAL = 1; + + /** + * Specifies whether the field supports both internal and external URLs. + */ + const LINK_GENERIC = 2; /** * Definitions of the contained properties. @@ -39,13 +57,19 @@ class LinkItem extends ConfigFieldItemBase { */ public function getPropertyDefinitions() { if (!isset(static::$propertyDefinitions)) { - static::$propertyDefinitions['url'] = DataDefinition::create('uri') + static::$propertyDefinitions['url'] = DataDefinition::create('string') ->setLabel(t('URL')); static::$propertyDefinitions['title'] = DataDefinition::create('string') ->setLabel(t('Link text')); - static::$propertyDefinitions['attributes'] = DataDefinition::create('map') + static::$propertyDefinitions['route_name'] = DataDefinition::create('string') + ->setLabel(t('Route name')); + + static::$propertyDefinitions['route_parameters'] = DataDefinition::create('map') + ->setLabel(t('Route parameters')); + + static::$propertyDefinitions['options'] = DataDefinition::create('map') ->setLabel(t('Attributes')); } return static::$propertyDefinitions; @@ -69,8 +93,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, @@ -86,6 +123,17 @@ public static function schema(FieldDefinitionInterface $field_definition) { public function instanceSettingsForm(array $form, array &$form_state) { $element = array(); + $element['url_type'] = array( + '#type' => 'radios', + '#title' => t('Allowed url type'), + '#default_value' => $this->getSetting('url_type'), + '#options' => array( + static::LINK_INTERNAL => t('Internal urls only'), + static::LINK_EXTERNAL => t('External urls only'), + static::LINK_GENERIC => t('Both internal and external urls'), + ), + ); + $element['title'] = array( '#type' => 'radios', '#title' => t('Allow link text'), @@ -107,6 +155,13 @@ public function preSave() { // Trim any spaces around the URL and link text. $this->url = trim($this->url); $this->title = trim($this->title); + + // Split out the link 'query' and 'fragment' parts. + $parsed_url = UrlHelper::parse($this->url); + $this->options = array( + 'query' => $parsed_url['query'], + 'fragment' => $parsed_url['fragment'], + ) + $this->options; } /** @@ -117,4 +172,12 @@ public function isEmpty() { return $value === NULL || $value === ''; } + /** + * {@inheritdoc} + */ + public function isExternal() { + // External URLs 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 dc10574..bf80a66 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,11 @@ namespace Drupal\link\Plugin\Field\FieldWidget; +use Drupal\Component\Utility\Url as UrlHelper; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; +use Drupal\Core\Url; +use Drupal\link\Plugin\Field\FieldType\LinkItem; /** * Plugin implementation of the 'link' widget. @@ -31,6 +34,8 @@ class LinkWidget extends WidgetBase { * {@inheritdoc} */ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, array &$form_state) { + $url_type = $this->getFieldSetting('url_type'); + $element['url'] = array( '#type' => 'url', '#title' => t('URL'), @@ -39,6 +44,26 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#maxlength' => 2048, '#required' => $element['#required'], ); + + // If the field is configured to allow only internal or both internal and + // external paths, it cannot use the 'url' form element and we have to do + // the validation ourselves. + if ($url_type != LinkItem::LINK_EXTERNAL) { + $element['url']['#type'] = 'textfield'; + $element['#element_validate'][] = array($this, 'validateUrl'); + } + + // If the field is configured to allow only internal paths, add a useful + // element prefix. + if ($url_type == LinkItem::LINK_INTERNAL) { + $element['url']['#field_prefix'] = \Drupal::url('', array(), array('absolute' => TRUE)); + } + // If the field is configured to allow both internal and external paths, + // show a useful description + elseif ($url_type == LinkItem::LINK_GENERIC) { + $element['url']['#description'] = 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'), @@ -52,7 +77,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 @@ -60,7 +85,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')), ); @@ -125,17 +150,58 @@ public function settingsSummary() { 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']))); } } -} + /** + * Form element validation handler; Validates the url property. + */ + public function validateUrl(&$element, &$form_state, $form) { + $url_type = $this->getFieldSetting('url_type'); + + // Validate only if the field type supports external and/or internal URLs. + if ($url_type != LinkItem::LINK_EXTERNAL) { + try { + $url = Url::createFromPath($element['url']['#value']); + + if ($url->isExternal() && !UrlHelper::isValid($element['url']['#value'])) { + form_error($element['url'], $form_state, t('%url is not a valid external url.', array('%url' => $element['url']['#value']))); + } + } + catch (\Exception $e) { + form_error($element['url'], $form_state, t('%url is not a valid internal url.', array('%url' => $element['url']['#value']))); + } + } + } + + /** + * {@inheritdoc} + */ + public function massageFormValues(array $values, array $form, array &$form_state) { + foreach ($values as &$value) { + if (!empty($value['url'])) { + try { + $url = Url::createFromPath($value['url']); + $url->setOption('attributes', $value['attributes']); + + $value += $url->toArray(); + } + catch (\Exception $e) { + // Nothing to do here, validateUrl() already emitted a form validation + // error. + } + } + } + return $values; + } + +} diff --git a/core/modules/link/link.module b/core/modules/link/link.module index 24c6286..4dee62a 100644 --- a/core/modules/link/link.module +++ b/core/modules/link/link.module @@ -5,6 +5,8 @@ * Defines simple link field types. */ +use Drupal\Component\Utility\String; + /** * Implements hook_help(). */ @@ -13,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') . '
'; @@ -25,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; } @@ -37,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', ), ); @@ -55,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'] = check_plain($variables['title']); + $variables['title'] = String::checkPlain($variables['title']); + } + + if (!$variables['url']->isExternal()) { + $variables['link'] = \Drupal::linkGenerator()->generateFromUrl($variables['url_title'], $variables['url']); + } + else { + $variables['link'] = l($variables['url_title'], $variables['url']->toString(), $variables['url']->getOptions()); } - $variables['link'] = l($variables['url_title'], $variables['href'], $variables['options']); }