Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 169 |
LinkWidget | |
0.00% |
0 / 1 |
|
0.00% |
0 / 12 |
2352 | |
0.00% |
0 / 169 |
defaultSettings | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 5 |
|||
getUriAsDisplayableString | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 19 |
|||
getUserEnteredStringAsUri | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 13 |
|||
validateUriElement | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 7 |
|||
validateTitleElement | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 5 |
|||
formElement | |
0.00% |
0 / 1 |
272 | |
0.00% |
0 / 54 |
|||
supportsInternalLinks | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
supportsExternalLinks | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
settingsForm | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 20 |
|||
settingsSummary | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 16 |
|||
massageFormValues | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 6 |
|||
flagErrors | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 18 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\link\Plugin\Field\FieldWidget\LinkWidget. | |
*/ | |
namespace Drupal\link\Plugin\Field\FieldWidget; | |
use Drupal\Core\Entity\Element\EntityAutocomplete; | |
use Drupal\Core\Field\FieldItemListInterface; | |
use Drupal\Core\Field\WidgetBase; | |
use Drupal\Core\Form\FormStateInterface; | |
use Drupal\link\LinkItemInterface; | |
use Symfony\Component\Validator\ConstraintViolation; | |
use Symfony\Component\Validator\ConstraintViolationListInterface; | |
/** | |
* Plugin implementation of the 'link' widget. | |
* | |
* @FieldWidget( | |
* id = "link_default", | |
* label = @Translation("Link"), | |
* field_types = { | |
* "link" | |
* } | |
* ) | |
*/ | |
class LinkWidget extends WidgetBase { | |
/** | |
* {@inheritdoc} | |
*/ | |
public static function defaultSettings() { | |
return array( | |
'placeholder_url' => '', | |
'placeholder_title' => '', | |
) + parent::defaultSettings(); | |
} | |
/** | |
* Gets the URI without the 'internal:' or 'entity:' scheme. | |
* | |
* The following two forms of URIs are transformed: | |
* - 'entity:' URIs: to entity autocomplete ("label (entity id)") strings; | |
* - 'internal:' URIs: the scheme is stripped. | |
* | |
* This method is the inverse of ::getUserEnteredStringAsUri(). | |
* | |
* @param string $uri | |
* The URI to get the displayable string for. | |
* | |
* @return string | |
* | |
* @see static::getUserEnteredStringAsUri() | |
*/ | |
protected static function getUriAsDisplayableString($uri) { | |
$scheme = parse_url($uri, PHP_URL_SCHEME); | |
// By default, the displayable string is the URI. | |
$displayable_string = $uri; | |
// A different displayable string may be chosen in case of the 'internal:' | |
// or 'entity:' built-in schemes. | |
if ($scheme === 'internal') { | |
$uri_reference = explode(':', $uri, 2)[1]; | |
// @todo '<front>' is valid input for BC reasons, may be removed by | |
// https://www.drupal.org/node/2421941 | |
$path = parse_url($uri, PHP_URL_PATH); | |
if ($path === '/') { | |
$uri_reference = '<front>' . substr($uri_reference, 1); | |
} | |
$displayable_string = $uri_reference; | |
} | |
elseif ($scheme === 'entity') { | |
list($entity_type, $entity_id) = explode('/', substr($uri, 7), 2); | |
// Show the 'entity:' URI as the entity autocomplete would. | |
$entity_manager = \Drupal::entityManager(); | |
if ($entity_manager->getDefinition($entity_type, FALSE) && $entity = \Drupal::entityManager()->getStorage($entity_type)->load($entity_id)) { | |
$displayable_string = EntityAutocomplete::getEntityLabels(array($entity)); | |
} | |
} | |
return $displayable_string; | |
} | |
/** | |
* Gets the user-entered string as a URI. | |
* | |
* The following two forms of input are mapped to URIs: | |
* - entity autocomplete ("label (entity id)") strings: to 'entity:' URIs; | |
* - strings without a detectable scheme: to 'internal:' URIs. | |
* | |
* This method is the inverse of ::getUriAsDisplayableString(). | |
* | |
* @param string $string | |
* The user-entered string. | |
* | |
* @return string | |
* The URI, if a non-empty $uri was passed. | |
* | |
* @see static::getUriAsDisplayableString() | |
*/ | |
protected static function getUserEnteredStringAsUri($string) { | |
// By default, assume the entered string is an URI. | |
$uri = $string; | |
// Detect entity autocomplete string, map to 'entity:' URI. | |
$entity_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($string); | |
if ($entity_id !== NULL) { | |
// @todo Support entity types other than 'node'. Will be fixed in | |
// https://www.drupal.org/node/2423093. | |
$uri = 'entity:node/' . $entity_id; | |
} | |
// Detect a schemeless string, map to 'internal:' URI. | |
elseif (!empty($string) && parse_url($string, PHP_URL_SCHEME) === NULL) { | |
// @todo '<front>' is valid input for BC reasons, may be removed by | |
// https://www.drupal.org/node/2421941 | |
// - '<front>' -> '/' | |
// - '<front>#foo' -> '/#foo' | |
if (strpos($string, '<front>') === 0) { | |
$string = '/' . substr($string, strlen('<front>')); | |
} | |
$uri = 'internal:' . $string; | |
} | |
return $uri; | |
} | |
/** | |
* Form element validation handler for the 'uri' element. | |
* | |
* Disallows saving inaccessible or untrusted URLs. | |
*/ | |
public static function validateUriElement($element, FormStateInterface $form_state, $form) { | |
$uri = static::getUserEnteredStringAsUri($element['#value']); | |
$form_state->setValueForElement($element, $uri); | |
// If getUserEnteredStringAsUri() mapped the entered value to a 'internal:' | |
// URI , ensure the raw value begins with '/', '?' or '#'. | |
// @todo '<front>' is valid input for BC reasons, may be removed by | |
// https://www.drupal.org/node/2421941 | |
if (parse_url($uri, PHP_URL_SCHEME) === 'internal' && !in_array($element['#value'][0], ['/', '?', '#'], TRUE) && substr($element['#value'], 0, 7) !== '<front>') { | |
$form_state->setError($element, t('Manually entered paths should start with /, ? or #.')); | |
return; | |
} | |
} | |
/** | |
* Form element validation handler for the 'title' element. | |
* | |
* Conditionally requires the link title if a URL value was filled in. | |
*/ | |
public static function validateTitleElement(&$element, FormStateInterface $form_state, $form) { | |
if ($element['uri']['#value'] !== '' && $element['title']['#value'] === '') { | |
$element['title']['#required'] = TRUE; | |
// We expect the field name placeholder value to be wrapped in t() here, | |
// so it won't be escaped again as it's already marked safe. | |
$form_state->setError($element['title'], t('@name field is required.', array('@name' => $element['title']['#title']))); | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { | |
/** @var \Drupal\link\LinkItemInterface $item */ | |
$item = $items[$delta]; | |
$element['uri'] = array( | |
'#type' => 'url', | |
'#title' => $this->t('URL'), | |
'#placeholder' => $this->getSetting('placeholder_url'), | |
// The current field value could have been entered by a different user. | |
// However, if it is inaccessible to the current user, do not display it | |
// to them. | |
'#default_value' => (!$item->isEmpty() && (\Drupal::currentUser()->hasPermission('link to any page') || $item->getUrl()->access())) ? static::getUriAsDisplayableString($item->uri) : NULL, | |
'#element_validate' => array(array(get_called_class(), 'validateUriElement')), | |
'#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['uri']['#type'] = 'entity_autocomplete'; | |
// @todo The user should be able to select an entity type. Will be fixed | |
// in https://www.drupal.org/node/2423093. | |
$element['uri']['#target_type'] = 'node'; | |
// Disable autocompletion when the first character is '/', '#' or '?'. | |
$element['uri']['#attributes']['data-autocomplete-first-character-blacklist'] = '/#?'; | |
// The link widget is doing its own processing in | |
// static::getUriAsDisplayableString(). | |
$element['uri']['#process_default_value'] = FALSE; | |
} | |
// If the field is configured to allow only internal links, add a useful | |
// element prefix. | |
if (!$this->supportsExternalLinks()) { | |
$element['uri']['#field_prefix'] = rtrim(\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['uri']['#description'] = $this->t('Start typing the title of a piece of content to select it. You can also enter an internal path such as %add-node or an external URL such as %url. Enter %front to link to the front page.', array('%front' => '<front>', '%add-node' => '/node/add', '%url' => 'http://example.com')); | |
} | |
// If the field is configured to allow only external links, show a useful | |
// description. | |
elseif ($this->supportsExternalLinks() && !$this->supportsInternalLinks()) { | |
$element['uri']['#description'] = $this->t('This must be an external URL such as %url.', array('%url' => 'http://example.com')); | |
} | |
$element['title'] = array( | |
'#type' => 'textfield', | |
'#title' => $this->t('Link text'), | |
'#placeholder' => $this->getSetting('placeholder_title'), | |
'#default_value' => isset($items[$delta]->title) ? $items[$delta]->title : NULL, | |
'#maxlength' => 255, | |
'#access' => $this->getFieldSetting('title') != DRUPAL_DISABLED, | |
); | |
// Post-process the title field to make it conditionally required if URL is | |
// non-empty. Omit the validation on the field edit form, since the field | |
// settings cannot be saved otherwise. | |
if (!$this->isDefaultValueWidget($form_state) && $this->getFieldSetting('title') == DRUPAL_REQUIRED) { | |
$element['#element_validate'][] = array(get_called_class(), 'validateTitleElement'); | |
} | |
// Exposing the attributes array in the widget is left for alternate and more | |
// advanced field widgets. | |
$element['attributes'] = array( | |
'#type' => 'value', | |
'#tree' => TRUE, | |
'#value' => !empty($items[$delta]->options['attributes']) ? $items[$delta]->options['attributes'] : array(), | |
'#attributes' => array('class' => array('link-field-widget-attributes')), | |
); | |
// If cardinality is 1, ensure a proper label is output for the field. | |
if ($this->fieldDefinition->getFieldStorageDefinition()->getCardinality() == 1) { | |
// If the link title is disabled, use the field definition label as the | |
// title of the 'uri' element. | |
if ($this->getFieldSetting('title') == DRUPAL_DISABLED) { | |
$element['uri']['#title'] = $element['#title']; | |
} | |
// Otherwise wrap everything in a details element. | |
else { | |
$element += array( | |
'#type' => 'fieldset', | |
); | |
} | |
} | |
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} | |
*/ | |
public function settingsForm(array $form, FormStateInterface $form_state) { | |
$elements = parent::settingsForm($form, $form_state); | |
$elements['placeholder_url'] = array( | |
'#type' => 'textfield', | |
'#title' => $this->t('Placeholder for URL'), | |
'#default_value' => $this->getSetting('placeholder_url'), | |
'#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' => $this->t('Placeholder for link text'), | |
'#default_value' => $this->getSetting('placeholder_title'), | |
'#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), | |
), | |
), | |
); | |
return $elements; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function settingsSummary() { | |
$summary = array(); | |
$placeholder_title = $this->getSetting('placeholder_title'); | |
$placeholder_url = $this->getSetting('placeholder_url'); | |
if (empty($placeholder_title) && empty($placeholder_url)) { | |
$summary[] = $this->t('No placeholders'); | |
} | |
else { | |
if (!empty($placeholder_title)) { | |
$summary[] = $this->t('Title placeholder: @placeholder_title', array('@placeholder_title' => $placeholder_title)); | |
} | |
if (!empty($placeholder_url)) { | |
$summary[] = $this->t('URL placeholder: @placeholder_url', array('@placeholder_url' => $placeholder_url)); | |
} | |
} | |
return $summary; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { | |
foreach ($values as &$value) { | |
$value['uri'] = static::getUserEnteredStringAsUri($value['uri']); | |
$value += ['options' => []]; | |
} | |
return $values; | |
} | |
/** | |
* {@inheritdoc} | |
* | |
* Override the '%uri' message parameter, to ensure that 'internal:' URIs | |
* show a validation error message that doesn't mention that scheme. | |
*/ | |
public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) { | |
/** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */ | |
foreach ($violations as $offset => $violation) { | |
$parameters = $violation->getParameters(); | |
if (isset($parameters['@uri'])) { | |
$parameters['@uri'] = static::getUriAsDisplayableString($parameters['@uri']); | |
$violations->set($offset, new ConstraintViolation( | |
$this->t($violation->getMessageTemplate(), $parameters), | |
$violation->getMessageTemplate(), | |
$parameters, | |
$violation->getRoot(), | |
$violation->getPropertyPath(), | |
$violation->getInvalidValue(), | |
$violation->getPlural(), | |
$violation->getCode() | |
)); | |
} | |
} | |
parent::flagErrors($items, $violations, $form, $form_state); | |
} | |
} |