Initial Drupal 11 with DDEV setup
This commit is contained in:
206
web/core/modules/link/src/AttributeXss.php
Normal file
206
web/core/modules/link/src/AttributeXss.php
Normal file
@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\link;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
|
||||
/**
|
||||
* Defines a class for attribute XSS filtering.
|
||||
*
|
||||
* @internal This class was added for a security fix and will be folded into
|
||||
* the \Drupal\Component\Utility\Xss class in a public issue.
|
||||
*/
|
||||
final class AttributeXss {
|
||||
|
||||
/**
|
||||
* Filters attributes.
|
||||
*
|
||||
* @param string $attributes
|
||||
* Rendered attribute string, e.g. 'class="foo bar"'.
|
||||
*/
|
||||
private static function attributes(string $attributes): array {
|
||||
$attributes_array = [];
|
||||
$mode = 0;
|
||||
$attribute_name = '';
|
||||
$skip = FALSE;
|
||||
$skip_protocol_filtering = FALSE;
|
||||
|
||||
while (strlen($attributes) != 0) {
|
||||
// Was the last operation successful?
|
||||
$working = 0;
|
||||
|
||||
switch ($mode) {
|
||||
case 0:
|
||||
// Attribute name, href for instance.
|
||||
if (preg_match('/^([-a-zA-Z][-a-zA-Z0-9]*)/', $attributes, $match)) {
|
||||
$attribute_name = strtolower($match[1]);
|
||||
$skip = (
|
||||
$attribute_name == 'style' ||
|
||||
str_starts_with($attribute_name, 'on') ||
|
||||
str_starts_with($attribute_name, '-') ||
|
||||
// Ignore long attributes to avoid unnecessary processing
|
||||
// overhead.
|
||||
strlen($attribute_name) > 96
|
||||
);
|
||||
|
||||
// Values for attributes of type URI should be filtered for
|
||||
// potentially malicious protocols (for example, an href-attribute
|
||||
// starting with "javascript:"). However, for some non-URI
|
||||
// attributes performing this filtering causes valid and safe data
|
||||
// to be mangled. We prevent this by skipping protocol filtering on
|
||||
// such attributes.
|
||||
// @see \Drupal\Component\Utility\UrlHelper::filterBadProtocol()
|
||||
// @see http://www.w3.org/TR/html4/index/attributes.html
|
||||
$skip_protocol_filtering = str_starts_with($attribute_name, 'data-') || in_array($attribute_name, [
|
||||
'title',
|
||||
'alt',
|
||||
'rel',
|
||||
'property',
|
||||
'class',
|
||||
'datetime',
|
||||
]);
|
||||
|
||||
$working = $mode = 1;
|
||||
$attributes = preg_replace('/^[-a-zA-Z][-a-zA-Z0-9]*/', '', $attributes);
|
||||
}
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// Equals sign or valueless ("selected").
|
||||
if (preg_match('/^\s*=\s*/', $attributes)) {
|
||||
$working = 1;
|
||||
$mode = 2;
|
||||
$attributes = preg_replace('/^\s*=\s*/', '', $attributes);
|
||||
break;
|
||||
}
|
||||
|
||||
if (preg_match('/^\s+/', $attributes)) {
|
||||
$working = 1;
|
||||
$mode = 0;
|
||||
if (!$skip) {
|
||||
$attributes_array[$attribute_name] = $attribute_name;
|
||||
}
|
||||
$attributes = preg_replace('/^\s+/', '', $attributes);
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Once we've finished processing the attribute value continue to look
|
||||
// for attributes.
|
||||
$mode = 0;
|
||||
$working = 1;
|
||||
// Attribute value, a URL after href= for instance.
|
||||
if (preg_match('/^"([^"]*)"(\s+|$)/', $attributes, $match)) {
|
||||
$value = $skip_protocol_filtering ? $match[1] : UrlHelper::filterBadProtocol($match[1]);
|
||||
|
||||
if (!$skip) {
|
||||
$attributes_array[$attribute_name] = $value;
|
||||
}
|
||||
$attributes = preg_replace('/^"[^"]*"(\s+|$)/', '', $attributes);
|
||||
break;
|
||||
}
|
||||
|
||||
if (preg_match("/^'([^']*)'(\s+|$)/", $attributes, $match)) {
|
||||
$value = $skip_protocol_filtering ? $match[1] : UrlHelper::filterBadProtocol($match[1]);
|
||||
|
||||
if (!$skip) {
|
||||
$attributes_array[$attribute_name] = $value;
|
||||
}
|
||||
$attributes = preg_replace("/^'[^']*'(\s+|$)/", '', $attributes);
|
||||
break;
|
||||
}
|
||||
|
||||
if (preg_match("%^([^\s\"']+)(\s+|$)%", $attributes, $match)) {
|
||||
$value = $skip_protocol_filtering ? $match[1] : UrlHelper::filterBadProtocol($match[1]);
|
||||
|
||||
if (!$skip) {
|
||||
$attributes_array[$attribute_name] = $value;
|
||||
}
|
||||
$attributes = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attributes);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($working == 0) {
|
||||
// Not well-formed; remove and try again.
|
||||
$attributes = preg_replace('/
|
||||
^
|
||||
(
|
||||
"[^"]*("|$) # - a string that starts with a double quote, up until the next double quote or the end of the string
|
||||
| # or
|
||||
\'[^\']*(\'|$)| # - a string that starts with a quote, up until the next quote or the end of the string
|
||||
| # or
|
||||
\S # - a non-whitespace character
|
||||
)* # any number of the above three
|
||||
\s* # any number of whitespaces
|
||||
/x', '', $attributes);
|
||||
$mode = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// The attribute list ends with a valueless attribute like "selected".
|
||||
if ($mode == 1 && !$skip) {
|
||||
$attributes_array[$attribute_name] = $attribute_name;
|
||||
}
|
||||
return $attributes_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes attributes.
|
||||
*
|
||||
* @param array $attributes
|
||||
* Attribute values as key => value format. Value may be a string or in the
|
||||
* case of the 'class' attribute, an array.
|
||||
*
|
||||
* @return array
|
||||
* Sanitized attributes.
|
||||
*/
|
||||
public static function sanitizeAttributes(array $attributes): array {
|
||||
$new_attributes = [];
|
||||
foreach ($attributes as $name => $value) {
|
||||
// The attribute name should be a single attribute, but there is the
|
||||
// possibility that the name is corrupt. Core's XSS::attributes can
|
||||
// cleanly handle sanitizing 'selected href="http://example.com" so we
|
||||
// provide an allowance for cases where the attribute array is malformed.
|
||||
// For example given a name of 'selected href' and a value of
|
||||
// http://example.com we split this into two separate attributes, with the
|
||||
// value assigned to the last attribute name.
|
||||
// Explode the attribute name if a space exists.
|
||||
$names = \array_filter(\explode(' ', $name));
|
||||
if (\count($names) === 0) {
|
||||
// Empty attribute names.
|
||||
continue;
|
||||
}
|
||||
// Valueless attributes set the name to the value when processed by the
|
||||
// Attributes object.
|
||||
$with_values = \array_combine($names, $names);
|
||||
// Create a new Attribute object with the value applied to the last
|
||||
// attribute name. If there is only one attribute this simply creates a
|
||||
// new attribute with a single key-value pair.
|
||||
$last_name = \end($names);
|
||||
$with_values[$last_name] = $value;
|
||||
$attribute_object = new Attribute($with_values);
|
||||
// Filter the attributes.
|
||||
$safe = AttributeXss::attributes((string) $attribute_object);
|
||||
$safe = \array_map([Html::class, 'decodeEntities'], $safe);
|
||||
if (\array_key_exists('class', $safe)) {
|
||||
// The class attribute is expected to be an array.
|
||||
$safe['class'] = \explode(' ', $safe['class']);
|
||||
}
|
||||
// Special case for boolean values which are unique to valueless
|
||||
// attributes.
|
||||
if (\array_key_exists($last_name, $safe) && \is_bool($value)) {
|
||||
$safe[$last_name] = $value;
|
||||
}
|
||||
// Add the safe attributes to the new list.
|
||||
$new_attributes += \array_intersect_key($safe, $with_values);
|
||||
}
|
||||
|
||||
return $new_attributes;
|
||||
}
|
||||
|
||||
}
|
||||
86
web/core/modules/link/src/Hook/LinkHooks.php
Normal file
86
web/core/modules/link/src/Hook/LinkHooks.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Hook;
|
||||
|
||||
use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
|
||||
/**
|
||||
* Hook implementations for link.
|
||||
*/
|
||||
class LinkHooks {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
#[Hook('help')]
|
||||
public function help($route_name, RouteMatchInterface $route_match): ?string {
|
||||
switch ($route_name) {
|
||||
case 'help.page.link':
|
||||
$output = '';
|
||||
$output .= '<h2>' . $this->t('About') . '</h2>';
|
||||
$output .= '<p>' . $this->t('The Link module allows you to create fields that contain internal or external URLs and optional link text. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":link_documentation">online documentation for the Link module</a>.', [
|
||||
':field' => Url::fromRoute('help.page', [
|
||||
'name' => 'field',
|
||||
])->toString(),
|
||||
':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
|
||||
'name' => 'field_ui',
|
||||
])->toString() : '#',
|
||||
':link_documentation' => 'https://www.drupal.org/documentation/modules/link',
|
||||
]) . '</p>';
|
||||
$output .= '<h2>' . $this->t('Uses') . '</h2>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . $this->t('Managing and displaying link fields') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('The <em>settings</em> and the <em>display</em> of the link field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [
|
||||
':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
|
||||
'name' => 'field_ui',
|
||||
])->toString() : '#',
|
||||
]) . '</dd>';
|
||||
$output .= '<dt>' . $this->t('Setting the allowed link type') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('In the field settings you can define the allowed link type to be <em>internal links only</em>, <em>external links only</em>, or <em>both internal and external links</em>. <em>Internal links only</em> and <em>both internal and external links</em> options enable an autocomplete widget for internal links, so a user does not have to copy or remember a URL.') . '</dd>';
|
||||
$output .= '<dt>' . $this->t('Adding link text') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('In the field settings you can define additional link text to be <em>optional</em> or <em>required</em> in any link field.') . '</dd>';
|
||||
$output .= '<dt>' . $this->t('Displaying link text') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('If link text has been submitted for a URL, then by default this link text is displayed as a link to the URL. If you want to display both the link text <em>and</em> the URL, choose the appropriate link format from the drop-down menu in the <em>Manage display</em> page. If you only want to display the URL even if link text has been submitted, choose <em>Link</em> as the format, and then change its <em>Format settings</em> to display <em>URL only</em>.') . '</dd>';
|
||||
$output .= '<dt>' . $this->t('Adding attributes to links') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('You can add attributes to links, by changing the <em>Format settings</em> in the <em>Manage display</em> page. Adding <em>rel="nofollow"</em> notifies search engines that links should not be followed.') . '</dd>';
|
||||
$output .= '<dt>' . $this->t('Validating URLs') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('All links are validated after a link field is filled in. They can include anchors or query strings.') . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_theme().
|
||||
*/
|
||||
#[Hook('theme')]
|
||||
public function theme() : array {
|
||||
return [
|
||||
'link_formatter_link_separate' => [
|
||||
'variables' => [
|
||||
'title' => NULL,
|
||||
'url_title' => NULL,
|
||||
'url' => NULL,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_field_type_category_info_alter().
|
||||
*/
|
||||
#[Hook('field_type_category_info_alter')]
|
||||
public function fieldTypeCategoryInfoAlter(&$definitions): void {
|
||||
// The `link` field type belongs in the `general` category, so the libraries
|
||||
// need to be attached using an alter hook.
|
||||
$definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'link/drupal.link-icon';
|
||||
}
|
||||
|
||||
}
|
||||
54
web/core/modules/link/src/LinkItemInterface.php
Normal file
54
web/core/modules/link/src/LinkItemInterface.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
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();
|
||||
|
||||
/**
|
||||
* Gets the URL object.
|
||||
*
|
||||
* @return \Drupal\Core\Url
|
||||
* Returns a Url object.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* Thrown when there is a problem with field data.
|
||||
*/
|
||||
public function getUrl();
|
||||
|
||||
/**
|
||||
* Gets the link title.
|
||||
*
|
||||
* @return string|null
|
||||
* Returns the link title.
|
||||
*/
|
||||
public function getTitle(): ?string;
|
||||
|
||||
}
|
||||
@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\Field\FieldFormatter;
|
||||
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
use Drupal\Core\Field\Attribute\FieldFormatter;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Field\FormatterBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Path\PathValidatorInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\link\AttributeXss;
|
||||
use Drupal\link\LinkItemInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'link' formatter.
|
||||
*/
|
||||
#[FieldFormatter(
|
||||
id: 'link',
|
||||
label: new TranslatableMarkup('Link'),
|
||||
field_types: [
|
||||
'link',
|
||||
],
|
||||
)]
|
||||
class LinkFormatter extends FormatterBase {
|
||||
|
||||
/**
|
||||
* The path validator service.
|
||||
*
|
||||
* @var \Drupal\Core\Path\PathValidatorInterface
|
||||
*/
|
||||
protected $pathValidator;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$configuration['field_definition'],
|
||||
$configuration['settings'],
|
||||
$configuration['label'],
|
||||
$configuration['view_mode'],
|
||||
$configuration['third_party_settings'],
|
||||
$container->get('path.validator')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new LinkFormatter.
|
||||
*
|
||||
* @param string $plugin_id
|
||||
* The plugin ID for the formatter.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||
* The definition of the field to which the formatter is associated.
|
||||
* @param array $settings
|
||||
* The formatter settings.
|
||||
* @param string $label
|
||||
* The formatter label display setting.
|
||||
* @param string $view_mode
|
||||
* The view mode.
|
||||
* @param array $third_party_settings
|
||||
* Third party settings.
|
||||
* @param \Drupal\Core\Path\PathValidatorInterface $path_validator
|
||||
* The path validator service.
|
||||
*/
|
||||
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, PathValidatorInterface $path_validator) {
|
||||
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
|
||||
$this->pathValidator = $path_validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function defaultSettings() {
|
||||
return [
|
||||
'trim_length' => '80',
|
||||
'url_only' => '',
|
||||
'url_plain' => '',
|
||||
'rel' => '',
|
||||
'target' => '',
|
||||
] + parent::defaultSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state) {
|
||||
$elements = parent::settingsForm($form, $form_state);
|
||||
|
||||
$elements['trim_length'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Trim link text length'),
|
||||
'#field_suffix' => $this->t('characters'),
|
||||
'#default_value' => $this->getSetting('trim_length'),
|
||||
'#min' => 1,
|
||||
'#description' => $this->t('Leave blank to allow unlimited link text lengths.'),
|
||||
];
|
||||
$elements['url_only'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('URL only'),
|
||||
'#default_value' => $this->getSetting('url_only'),
|
||||
'#access' => $this->getPluginId() == 'link',
|
||||
];
|
||||
$elements['url_plain'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Show URL as plain text'),
|
||||
'#default_value' => $this->getSetting('url_plain'),
|
||||
'#access' => $this->getPluginId() == 'link',
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name*="url_only"]' => ['checked' => TRUE],
|
||||
],
|
||||
],
|
||||
];
|
||||
$elements['rel'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Add rel="nofollow" to links'),
|
||||
'#return_value' => 'nofollow',
|
||||
'#default_value' => $this->getSetting('rel'),
|
||||
];
|
||||
$elements['target'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Open link in new window'),
|
||||
'#return_value' => '_blank',
|
||||
'#default_value' => $this->getSetting('target'),
|
||||
];
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsSummary() {
|
||||
$summary = [];
|
||||
|
||||
$settings = $this->getSettings();
|
||||
|
||||
if (!empty($settings['trim_length'])) {
|
||||
$summary[] = $this->t('Link text trimmed to @limit characters', ['@limit' => $settings['trim_length']]);
|
||||
}
|
||||
else {
|
||||
$summary[] = $this->t('Link text not trimmed');
|
||||
}
|
||||
if ($this->getPluginId() == 'link' && !empty($settings['url_only'])) {
|
||||
if (!empty($settings['url_plain'])) {
|
||||
$summary[] = $this->t('Show URL only as plain-text');
|
||||
}
|
||||
else {
|
||||
$summary[] = $this->t('Show URL only');
|
||||
}
|
||||
}
|
||||
if (!empty($settings['rel'])) {
|
||||
$summary[] = $this->t('Add rel="@rel"', ['@rel' => $settings['rel']]);
|
||||
}
|
||||
if (!empty($settings['target'])) {
|
||||
$summary[] = $this->t('Open link in new window');
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function viewElements(FieldItemListInterface $items, $langcode) {
|
||||
$element = [];
|
||||
$entity = $items->getEntity();
|
||||
$settings = $this->getSettings();
|
||||
|
||||
foreach ($items as $delta => $item) {
|
||||
// By default use the full URL as the link text.
|
||||
$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)) {
|
||||
// Unsanitized token replacement here because the entire link title
|
||||
// gets auto-escaped during link generation in
|
||||
// \Drupal\Core\Utility\LinkGenerator::generate().
|
||||
$link_title = \Drupal::token()->replace($item->title, [$entity->getEntityTypeId() => $entity], ['clear' => TRUE]);
|
||||
}
|
||||
|
||||
// Trim the link text to the desired length.
|
||||
if (!empty($settings['trim_length'])) {
|
||||
$link_title = Unicode::truncate($link_title, $settings['trim_length'], FALSE, TRUE);
|
||||
}
|
||||
|
||||
if (!empty($settings['url_only']) && !empty($settings['url_plain'])) {
|
||||
$element[$delta] = [
|
||||
'#plain_text' => $link_title,
|
||||
];
|
||||
|
||||
if (!empty($item->_attributes)) {
|
||||
// Piggyback on the metadata attributes, which will be placed in the
|
||||
// field template wrapper, and set the URL value in a content
|
||||
// attribute.
|
||||
$content = str_replace('internal:/', '', $item->uri);
|
||||
$item->_attributes += ['content' => $content];
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Skip the #options to prevent duplications of query parameters.
|
||||
$element[$delta] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $link_title,
|
||||
'#url' => $url,
|
||||
];
|
||||
|
||||
if (!empty($item->_attributes)) {
|
||||
$element[$delta]['#attributes'] = $item->_attributes;
|
||||
// Unset field item attributes since they have been included in the
|
||||
// formatter output and should not be rendered in the field template.
|
||||
unset($item->_attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the \Drupal\Core\Url object for a link field item.
|
||||
*
|
||||
* @param \Drupal\link\LinkItemInterface $item
|
||||
* The link field item being rendered.
|
||||
*
|
||||
* @return \Drupal\Core\Url
|
||||
* A Url object.
|
||||
*/
|
||||
protected function buildUrl(LinkItemInterface $item) {
|
||||
try {
|
||||
$url = $item->getUrl();
|
||||
}
|
||||
catch (\InvalidArgumentException) {
|
||||
// @todo Add logging here in https://www.drupal.org/project/drupal/issues/3348020
|
||||
$url = Url::fromRoute('<none>');
|
||||
}
|
||||
|
||||
$settings = $this->getSettings();
|
||||
$options = $item->options;
|
||||
$options += $url->getOptions();
|
||||
|
||||
// Add optional 'rel' attribute to link options.
|
||||
if (!empty($settings['rel'])) {
|
||||
$options['attributes']['rel'] = $settings['rel'];
|
||||
}
|
||||
// Add optional 'target' attribute to link options.
|
||||
if (!empty($settings['target'])) {
|
||||
$options['attributes']['target'] = $settings['target'];
|
||||
}
|
||||
|
||||
if (!empty($options['attributes'])) {
|
||||
$options['attributes'] = AttributeXss::sanitizeAttributes($options['attributes']);
|
||||
}
|
||||
|
||||
$url->setOptions($options);
|
||||
return $url;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\Field\FieldFormatter;
|
||||
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
use Drupal\Core\Field\Attribute\FieldFormatter;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'link_separate' formatter.
|
||||
*
|
||||
* @todo https://www.drupal.org/node/1829202 Merge into 'link' formatter once
|
||||
* there is a #type like 'item' that can render a compound label and content
|
||||
* outside of a form context.
|
||||
*/
|
||||
#[FieldFormatter(
|
||||
id: 'link_separate',
|
||||
label: new TranslatableMarkup('Separate link text and URL'),
|
||||
field_types: [
|
||||
'link',
|
||||
],
|
||||
)]
|
||||
class LinkSeparateFormatter extends LinkFormatter {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function defaultSettings() {
|
||||
return [
|
||||
'trim_length' => 80,
|
||||
'rel' => '',
|
||||
'target' => '',
|
||||
] + parent::defaultSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function viewElements(FieldItemListInterface $items, $langcode) {
|
||||
$element = [];
|
||||
$entity = $items->getEntity();
|
||||
$settings = $this->getSettings();
|
||||
|
||||
foreach ($items as $delta => $item) {
|
||||
// By default use the full URL as the link text.
|
||||
$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)) {
|
||||
// Unsanitized token replacement here because the entire link title
|
||||
// gets auto-escaped during link generation in
|
||||
// \Drupal\Core\Utility\LinkGenerator::generate().
|
||||
$link_title = \Drupal::token()->replace($item->title, [$entity->getEntityTypeId() => $entity], ['clear' => TRUE]);
|
||||
}
|
||||
|
||||
// The link_separate formatter has two titles; the link text (as in the
|
||||
// field values) and the URL itself. If there is no link text value,
|
||||
// $link_title defaults to the URL, so it needs to be unset.
|
||||
// The URL version may need to be trimmed as well.
|
||||
if (empty($item->title)) {
|
||||
$link_title = NULL;
|
||||
}
|
||||
$url_title = $url->toString();
|
||||
if (!empty($settings['trim_length'])) {
|
||||
$link_title = $link_title !== NULL ? Unicode::truncate($link_title, $settings['trim_length'], FALSE, TRUE) : NULL;
|
||||
$url_title = Unicode::truncate($url_title, $settings['trim_length'], FALSE, TRUE);
|
||||
}
|
||||
|
||||
$element[$delta] = [
|
||||
'#theme' => 'link_formatter_link_separate',
|
||||
'#title' => $link_title,
|
||||
'#url_title' => $url_title,
|
||||
'#url' => $url,
|
||||
];
|
||||
|
||||
if (!empty($item->_attributes)) {
|
||||
// Set our attributes on the <a> element that is being built.
|
||||
$url->setOption('attributes', $item->_attributes);
|
||||
|
||||
// Unset field item attributes since they have been included in the
|
||||
// formatter output and should not be rendered in the field template.
|
||||
unset($item->_attributes);
|
||||
}
|
||||
}
|
||||
return $element;
|
||||
}
|
||||
|
||||
}
|
||||
207
web/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
Normal file
207
web/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
Normal file
@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\Field\FieldType;
|
||||
|
||||
use Drupal\Component\Utility\Random;
|
||||
use Drupal\Core\Field\Attribute\FieldType;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldItemBase;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\TypedData\DataDefinition;
|
||||
use Drupal\Core\TypedData\MapDataDefinition;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\link\LinkItemInterface;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'link' field type.
|
||||
*/
|
||||
#[FieldType(
|
||||
id: "link",
|
||||
label: new TranslatableMarkup("Link"),
|
||||
description: new TranslatableMarkup("Stores a URL string, optional varchar link text, and optional blob of attributes to assemble a link."),
|
||||
default_widget: "link_default",
|
||||
default_formatter: "link",
|
||||
constraints: [
|
||||
"LinkType" => [],
|
||||
"LinkAccess" => [],
|
||||
"LinkExternalProtocols" => [],
|
||||
"LinkNotExistingInternal" => [],
|
||||
]
|
||||
)]
|
||||
class LinkItem extends FieldItemBase implements LinkItemInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function defaultFieldSettings() {
|
||||
return [
|
||||
'title' => DRUPAL_OPTIONAL,
|
||||
'link_type' => LinkItemInterface::LINK_GENERIC,
|
||||
] + parent::defaultFieldSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
|
||||
$properties['uri'] = DataDefinition::create('uri')
|
||||
->setLabel(new TranslatableMarkup('URI'));
|
||||
|
||||
$properties['title'] = DataDefinition::create('string')
|
||||
->setLabel(new TranslatableMarkup('Link text'));
|
||||
|
||||
$properties['options'] = MapDataDefinition::create()
|
||||
->setLabel(new TranslatableMarkup('Options'));
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function schema(FieldStorageDefinitionInterface $field_definition) {
|
||||
return [
|
||||
'columns' => [
|
||||
'uri' => [
|
||||
'description' => 'The URI of the link.',
|
||||
'type' => 'varchar',
|
||||
'length' => 2048,
|
||||
],
|
||||
'title' => [
|
||||
'description' => 'The link text.',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
],
|
||||
'options' => [
|
||||
'description' => 'Serialized array of options for the link.',
|
||||
'type' => 'blob',
|
||||
'size' => 'big',
|
||||
'serialize' => TRUE,
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
'uri' => [['uri', 30]],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
|
||||
$element = [];
|
||||
|
||||
$element['link_type'] = [
|
||||
'#type' => 'radios',
|
||||
'#title' => $this->t('Allowed link type'),
|
||||
'#default_value' => $this->getSetting('link_type'),
|
||||
'#options' => [
|
||||
static::LINK_INTERNAL => $this->t('Internal links only'),
|
||||
static::LINK_EXTERNAL => $this->t('External links only'),
|
||||
static::LINK_GENERIC => $this->t('Both internal and external links'),
|
||||
],
|
||||
];
|
||||
|
||||
$element['title'] = [
|
||||
'#type' => 'radios',
|
||||
'#title' => $this->t('Allow link text'),
|
||||
'#default_value' => $this->getSetting('title'),
|
||||
'#options' => [
|
||||
DRUPAL_DISABLED => $this->t('Disabled'),
|
||||
DRUPAL_OPTIONAL => $this->t('Optional'),
|
||||
DRUPAL_REQUIRED => $this->t('Required'),
|
||||
],
|
||||
];
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
|
||||
$random = new Random();
|
||||
if ($field_definition->getItemDefinition()->getSetting('link_type') & LinkItemInterface::LINK_EXTERNAL) {
|
||||
// Set of possible top-level domains.
|
||||
$tlds = ['com', 'net', 'gov', 'org', 'edu', 'biz', 'info'];
|
||||
// Set random length for the domain name.
|
||||
$domain_length = mt_rand(7, 15);
|
||||
|
||||
switch ($field_definition->getSetting('title')) {
|
||||
case DRUPAL_DISABLED:
|
||||
$values['title'] = '';
|
||||
break;
|
||||
|
||||
case DRUPAL_REQUIRED:
|
||||
$values['title'] = $random->sentences(4);
|
||||
break;
|
||||
|
||||
case DRUPAL_OPTIONAL:
|
||||
// In case of optional title, randomize its generation.
|
||||
$values['title'] = mt_rand(0, 1) ? $random->sentences(4) : '';
|
||||
break;
|
||||
}
|
||||
$values['uri'] = 'https://www.' . $random->word($domain_length) . '.' . $tlds[mt_rand(0, (count($tlds) - 1))];
|
||||
}
|
||||
else {
|
||||
$values['uri'] = 'base:' . $random->name(mt_rand(1, 64));
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isEmpty() {
|
||||
$value = $this->get('uri')->getValue();
|
||||
return $value === NULL || $value === '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isExternal() {
|
||||
return $this->getUrl()->isExternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function mainPropertyName() {
|
||||
return 'uri';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getUrl() {
|
||||
return Url::fromUri($this->uri, (array) $this->options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTitle(): ?string {
|
||||
return $this->title ?: NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setValue($values, $notify = TRUE) {
|
||||
// Treat the values as property value of the main property, if no array is
|
||||
// given.
|
||||
if (isset($values) && !is_array($values)) {
|
||||
$values = [static::mainPropertyName() => $values];
|
||||
}
|
||||
if (isset($values)) {
|
||||
$values += [
|
||||
'options' => [],
|
||||
];
|
||||
}
|
||||
parent::setValue($values, $notify);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,464 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\Field\FieldWidget;
|
||||
|
||||
use Drupal\Core\Field\Attribute\FieldWidget;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\Url;
|
||||
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: new TranslatableMarkup('Link'),
|
||||
field_types: ['link'],
|
||||
)]
|
||||
class LinkWidget extends WidgetBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function defaultSettings() {
|
||||
return [
|
||||
'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
|
||||
* The displayable string for the URI.
|
||||
*
|
||||
* @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') {
|
||||
[$entity_type, $entity_id] = explode('/', substr($uri, 7), 2);
|
||||
// Show the 'entity:' URI as the entity autocomplete would.
|
||||
// @todo Support entity types other than 'node'. Will be fixed in
|
||||
// https://www.drupal.org/node/2423093.
|
||||
if ($entity_type == 'node' && $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id)) {
|
||||
$displayable_string = EntityAutocomplete::getEntityLabels([$entity]);
|
||||
}
|
||||
}
|
||||
elseif ($scheme === 'route') {
|
||||
$displayable_string = ltrim($displayable_string, 'route:');
|
||||
}
|
||||
|
||||
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 a URI.
|
||||
$uri = trim($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;
|
||||
}
|
||||
// Support linking to nothing.
|
||||
elseif (in_array($string, ['<nolink>', '<none>', '<button>'], TRUE)) {
|
||||
$uri = 'route:' . $string;
|
||||
}
|
||||
// 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 (str_starts_with($string, '<front>')) {
|
||||
$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 an '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) && !str_starts_with($element['#value'], '<front>')) {
|
||||
$form_state->setError($element, new TranslatableMarkup('Manually entered paths should start with one of the following characters: / ? #'));
|
||||
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'] === '') {
|
||||
// We expect the field name placeholder value to be wrapped in $this->t()
|
||||
// here, so it won't be escaped again as it's already marked safe.
|
||||
$form_state->setError($element['title'], new TranslatableMarkup('@title field is required if there is @uri input.', ['@title' => $element['title']['#title'], '@uri' => $element['uri']['#title']]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form element validation handler for the 'title' element.
|
||||
*
|
||||
* Requires the URL value if a link title was filled in.
|
||||
*/
|
||||
public static function validateTitleNoLink(&$element, FormStateInterface $form_state, $form) {
|
||||
if ($element['uri']['#value'] === '' && $element['title']['#value'] !== '') {
|
||||
$form_state->setError($element['uri'], new TranslatableMarkup('The @uri field is required when the @title field is specified.', ['@title' => $element['title']['#title'], '@uri' => $element['uri']['#title']]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
|
||||
/** @var \Drupal\link\LinkItemInterface $item */
|
||||
$item = $items[$delta];
|
||||
|
||||
$display_uri = NULL;
|
||||
if (!$item->isEmpty()) {
|
||||
try {
|
||||
// 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.
|
||||
if (\Drupal::currentUser()->hasPermission('link to any page') || $item->getUrl()->access()) {
|
||||
$display_uri = static::getUriAsDisplayableString($item->uri);
|
||||
}
|
||||
}
|
||||
catch (\InvalidArgumentException) {
|
||||
// If $item->uri is invalid, show value as is, so the user can see what
|
||||
// to edit.
|
||||
// @todo Add logging here in https://www.drupal.org/project/drupal/issues/3348020
|
||||
$display_uri = $item->uri;
|
||||
}
|
||||
}
|
||||
$element['uri'] = [
|
||||
'#type' => 'url',
|
||||
'#title' => $this->t('URL'),
|
||||
'#placeholder' => $this->getSetting('placeholder_url'),
|
||||
'#default_value' => $display_uri,
|
||||
'#element_validate' => [[static::class, 'validateUriElement']],
|
||||
'#maxlength' => 2048,
|
||||
'#required' => $element['#required'],
|
||||
'#link_type' => $this->getFieldSetting('link_type'),
|
||||
];
|
||||
|
||||
// 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-denylist'] = '/#?';
|
||||
|
||||
// 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 and description.
|
||||
if (!$this->supportsExternalLinks()) {
|
||||
$element['uri']['#field_prefix'] = rtrim(Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString(), '/');
|
||||
$element['uri']['#description'] = $this->t('This must be an internal path such as %add-node. You can also start typing the title of a piece of content to select it. Enter %front to link to the front page. Enter %nolink to display link text only. Enter %button to display keyboard-accessible link text only.', ['%add-node' => '/node/add', '%front' => '<front>', '%nolink' => '<nolink>', '%button' => '<button>']);
|
||||
}
|
||||
// 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. Enter %nolink to display link text only. Enter %button to display keyboard-accessible link text only.', ['%front' => '<front>', '%add-node' => '/node/add', '%url' => 'https://example.com', '%nolink' => '<nolink>', '%button' => '<button>']);
|
||||
}
|
||||
// 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.', ['%url' => 'https://example.com']);
|
||||
}
|
||||
|
||||
// Make uri required on the front-end when title filled-in.
|
||||
if (!$this->isDefaultValueWidget($form_state) && $this->getFieldSetting('title') !== DRUPAL_DISABLED && !$element['uri']['#required']) {
|
||||
$parents = $element['#field_parents'];
|
||||
$parents[] = $this->fieldDefinition->getName();
|
||||
$selector = $root = array_shift($parents);
|
||||
if ($parents) {
|
||||
$selector = $root . '[' . implode('][', $parents) . ']';
|
||||
}
|
||||
|
||||
$element['uri']['#states']['required'] = [
|
||||
':input[name="' . $selector . '[' . $delta . '][title]"]' => ['filled' => TRUE],
|
||||
];
|
||||
}
|
||||
|
||||
$element['title'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Link text'),
|
||||
'#placeholder' => $this->getSetting('placeholder_title'),
|
||||
'#default_value' => $items[$delta]->title ?? NULL,
|
||||
'#maxlength' => 255,
|
||||
'#access' => $this->getFieldSetting('title') != DRUPAL_DISABLED,
|
||||
'#required' => $this->getFieldSetting('title') === DRUPAL_REQUIRED && $element['#required'],
|
||||
];
|
||||
// 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.
|
||||
//
|
||||
// Validate that title field is filled out (regardless of uri) when it is a
|
||||
// required field.
|
||||
if (!$this->isDefaultValueWidget($form_state) && $this->getFieldSetting('title') === DRUPAL_REQUIRED) {
|
||||
$element['#element_validate'][] = [static::class, 'validateTitleElement'];
|
||||
$element['#element_validate'][] = [static::class, 'validateTitleNoLink'];
|
||||
|
||||
if (!$element['title']['#required']) {
|
||||
// Make title required on the front-end when URI filled-in.
|
||||
|
||||
$parents = $element['#field_parents'];
|
||||
$parents[] = $this->fieldDefinition->getName();
|
||||
$selector = $root = array_shift($parents);
|
||||
if ($parents) {
|
||||
$selector = $root . '[' . implode('][', $parents) . ']';
|
||||
}
|
||||
|
||||
$element['title']['#states']['required'] = [
|
||||
':input[name="' . $selector . '[' . $delta . '][uri]"]' => ['filled' => TRUE],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that a URI is always entered when an optional title field is
|
||||
// submitted.
|
||||
if (!$this->isDefaultValueWidget($form_state) && $this->getFieldSetting('title') == DRUPAL_OPTIONAL) {
|
||||
$element['#element_validate'][] = [static::class, 'validateTitleNoLink'];
|
||||
}
|
||||
|
||||
// Exposing the attributes array in the widget is left for alternate and
|
||||
// more advanced field widgets.
|
||||
$element['attributes'] = [
|
||||
'#type' => 'value',
|
||||
'#tree' => TRUE,
|
||||
'#value' => !empty($items[$delta]->options['attributes']) ? $items[$delta]->options['attributes'] : [],
|
||||
'#attributes' => ['class' => ['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'];
|
||||
// By default the field description is added to the title field. Since
|
||||
// the title field is disabled, we add the description, if given, to the
|
||||
// uri element instead.
|
||||
if (!empty($element['#description'])) {
|
||||
if (empty($element['uri']['#description'])) {
|
||||
$element['uri']['#description'] = $element['#description'];
|
||||
}
|
||||
else {
|
||||
// If we have the description of the type of field together with
|
||||
// the user provided description, we want to make a distinction
|
||||
// between "core help text" and "user entered help text". To make
|
||||
// this distinction more clear, we put them in an unordered list.
|
||||
$element['uri']['#description'] = [
|
||||
'#theme' => 'item_list',
|
||||
'#items' => [
|
||||
// Assume the user-specified description has the most relevance,
|
||||
// so place it first.
|
||||
$element['#description'],
|
||||
$element['uri']['#description'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Otherwise wrap everything in a details element.
|
||||
else {
|
||||
$element += [
|
||||
'#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'] = [
|
||||
'#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'] = [
|
||||
'#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' => [
|
||||
'invisible' => [
|
||||
':input[name="instance[settings][title]"]' => ['value' => DRUPAL_DISABLED],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsSummary() {
|
||||
$summary = [];
|
||||
|
||||
$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', ['@placeholder_title' => $placeholder_title]);
|
||||
}
|
||||
if (!empty($placeholder_url)) {
|
||||
$summary[] = $this->t('URL placeholder: @placeholder_url', ['@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(
|
||||
// phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
|
||||
$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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\Validation\Attribute\Constraint;
|
||||
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
|
||||
|
||||
/**
|
||||
* Defines an access validation constraint for links.
|
||||
*/
|
||||
#[Constraint(
|
||||
id: 'LinkAccess',
|
||||
label: new TranslatableMarkup('Link URI can be accessed by the user.', [], ['context' => 'Validation'])
|
||||
)]
|
||||
class LinkAccessConstraint extends SymfonyConstraint {
|
||||
|
||||
/**
|
||||
* The error message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $message = "The path '@uri' is inaccessible.";
|
||||
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Core\Session\AccountProxyInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Validates the LinkAccess constraint.
|
||||
*/
|
||||
class LinkAccessConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* Proxy for the current user account.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountProxyInterface
|
||||
*/
|
||||
// phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName, Drupal.Commenting.VariableComment.Missing
|
||||
protected $current_user;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the LinkAccessConstraintValidator class.
|
||||
*
|
||||
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
|
||||
* The current user account.
|
||||
*/
|
||||
public function __construct(AccountProxyInterface $current_user) {
|
||||
$this->current_user = $current_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('current_user')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint): void {
|
||||
if (isset($value)) {
|
||||
try {
|
||||
$url = $value->getUrl();
|
||||
}
|
||||
// If the URL is malformed this constraint cannot check access.
|
||||
catch (\InvalidArgumentException) {
|
||||
return;
|
||||
}
|
||||
// Disallow URLs if the current user doesn't have the 'link to any page'
|
||||
// permission nor can access this URI.
|
||||
$allowed = $this->current_user->hasPermission('link to any page') || $url->access();
|
||||
if (!$allowed) {
|
||||
$this->context->addViolation($constraint->message, ['@uri' => $value->uri]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\Validation\Attribute\Constraint;
|
||||
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
|
||||
|
||||
/**
|
||||
* Defines a protocol validation constraint for links to external URLs.
|
||||
*/
|
||||
#[Constraint(
|
||||
id: 'LinkExternalProtocols',
|
||||
label: new TranslatableMarkup('No dangerous external protocols', [], ['context' => 'Validation'])
|
||||
)]
|
||||
class LinkExternalProtocolsConstraint extends SymfonyConstraint {
|
||||
|
||||
/**
|
||||
* The error message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $message = "The path '@uri' is invalid.";
|
||||
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Validates the LinkExternalProtocols constraint.
|
||||
*/
|
||||
class LinkExternalProtocolsConstraintValidator extends ConstraintValidator {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint): void {
|
||||
if (isset($value)) {
|
||||
try {
|
||||
/** @var \Drupal\Core\Url $url */
|
||||
$url = $value->getUrl();
|
||||
}
|
||||
// If the URL is malformed this constraint cannot check further.
|
||||
catch (\InvalidArgumentException) {
|
||||
return;
|
||||
}
|
||||
// Disallow external URLs using untrusted protocols.
|
||||
if ($url->isExternal() && !in_array(parse_url($url->getUri(), PHP_URL_SCHEME), UrlHelper::getAllowedProtocols())) {
|
||||
$this->context->addViolation($constraint->message, ['@uri' => $value->uri]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\Validation\Attribute\Constraint;
|
||||
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
|
||||
|
||||
/**
|
||||
* Defines a protocol validation constraint for links to broken internal URLs.
|
||||
*/
|
||||
#[Constraint(
|
||||
id: 'LinkNotExistingInternal',
|
||||
label: new TranslatableMarkup('No broken internal links', [], ['context' => 'Validation'])
|
||||
)]
|
||||
class LinkNotExistingInternalConstraint extends SymfonyConstraint {
|
||||
|
||||
/**
|
||||
* The error message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $message = "The path '@uri' is invalid.";
|
||||
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\Validation\Constraint;
|
||||
|
||||
use Symfony\Component\Routing\Exception\InvalidParameterException;
|
||||
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
|
||||
use Symfony\Component\Routing\Exception\RouteNotFoundException;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Validates the LinkNotExistingInternal constraint.
|
||||
*/
|
||||
class LinkNotExistingInternalConstraintValidator extends ConstraintValidator {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint): void {
|
||||
if (isset($value)) {
|
||||
try {
|
||||
/** @var \Drupal\Core\Url $url */
|
||||
$url = $value->getUrl();
|
||||
}
|
||||
// If the URL is malformed this constraint cannot check further.
|
||||
catch (\InvalidArgumentException) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($url->isRouted()) {
|
||||
$allowed = TRUE;
|
||||
try {
|
||||
$url->toString(TRUE);
|
||||
}
|
||||
// The following exceptions are all possible during URL generation, and
|
||||
// should be considered as disallowed URLs.
|
||||
catch (RouteNotFoundException) {
|
||||
$allowed = FALSE;
|
||||
}
|
||||
catch (InvalidParameterException) {
|
||||
$allowed = FALSE;
|
||||
}
|
||||
catch (MissingMandatoryParametersException) {
|
||||
$allowed = FALSE;
|
||||
}
|
||||
if (!$allowed) {
|
||||
$this->context->addViolation($constraint->message, ['@uri' => $value->uri]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\Validation\Attribute\Constraint;
|
||||
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
|
||||
|
||||
/**
|
||||
* Validation constraint for links receiving data allowed by its settings.
|
||||
*/
|
||||
#[Constraint(
|
||||
id: 'LinkType',
|
||||
label: new TranslatableMarkup('Link data valid for link type.', [], ['context' => 'Validation'])
|
||||
)]
|
||||
class LinkTypeConstraint extends SymfonyConstraint {
|
||||
|
||||
/**
|
||||
* The error message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $message = "The path '@uri' is invalid.";
|
||||
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\link\LinkItemInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Constraint validator for links receiving data allowed by its settings.
|
||||
*/
|
||||
class LinkTypeConstraintValidator extends ConstraintValidator {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint): void {
|
||||
if (isset($value)) {
|
||||
$uri_is_valid = TRUE;
|
||||
|
||||
/** @var \Drupal\link\LinkItemInterface $link_item */
|
||||
$link_item = $value;
|
||||
$link_type = $link_item->getFieldDefinition()->getSetting('link_type');
|
||||
|
||||
// Try to resolve the given URI to a URL. It may fail if it's schemeless.
|
||||
try {
|
||||
$url = $link_item->getUrl();
|
||||
}
|
||||
catch (\InvalidArgumentException) {
|
||||
$uri_is_valid = FALSE;
|
||||
}
|
||||
|
||||
// If the link field doesn't support both internal and external links,
|
||||
// check whether the URL (a resolved URI) is in fact violating either
|
||||
// restriction.
|
||||
if ($uri_is_valid && $link_type !== LinkItemInterface::LINK_GENERIC) {
|
||||
if (!($link_type & LinkItemInterface::LINK_EXTERNAL) && $url->isExternal()) {
|
||||
$uri_is_valid = FALSE;
|
||||
}
|
||||
if (!($link_type & LinkItemInterface::LINK_INTERNAL) && !$url->isExternal()) {
|
||||
$uri_is_valid = FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$uri_is_valid) {
|
||||
$this->context->addViolation($constraint->message, ['@uri' => $link_item->uri]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\migrate\field\d6;
|
||||
|
||||
use Drupal\migrate\Plugin\MigrationInterface;
|
||||
use Drupal\migrate_drupal\Attribute\MigrateField;
|
||||
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
|
||||
|
||||
/**
|
||||
* Migrate field plugin for Drupal 6 link fields.
|
||||
*/
|
||||
#[MigrateField(
|
||||
id: 'link',
|
||||
core: [6],
|
||||
type_map: [
|
||||
'link' => 'link',
|
||||
],
|
||||
source_module: 'link',
|
||||
destination_module: 'link',
|
||||
)]
|
||||
class LinkField extends FieldPluginBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFieldFormatterMap() {
|
||||
// See d6_field_formatter_settings.yml and FieldPluginBase
|
||||
// alterFieldFormatterMigration().
|
||||
return [
|
||||
'default' => 'link',
|
||||
'plain' => 'link',
|
||||
'absolute' => 'link',
|
||||
'title_plain' => 'link',
|
||||
'url' => 'link',
|
||||
'short' => 'link',
|
||||
'label' => 'link',
|
||||
'separate' => 'link_separate',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) {
|
||||
$process = [
|
||||
'plugin' => 'field_link',
|
||||
'source' => $field_name,
|
||||
];
|
||||
$migration->mergeProcessOfProperty($field_name, $process);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\migrate\field\d7;
|
||||
|
||||
use Drupal\link\Plugin\migrate\field\d6\LinkField as D6LinkField;
|
||||
use Drupal\migrate\Plugin\MigrationInterface;
|
||||
use Drupal\migrate_drupal\Attribute\MigrateField;
|
||||
|
||||
/**
|
||||
* MigrateField Plugin for Drupal 7 link fields.
|
||||
*
|
||||
* This plugin provides the exact same functionality as the Drupal 6 "link"
|
||||
* plugin with the exception that the plugin ID "link_field" is used in the
|
||||
* field type map.
|
||||
*/
|
||||
#[MigrateField(
|
||||
id: 'link_field',
|
||||
core: [7],
|
||||
type_map: [
|
||||
'link_field' => 'link',
|
||||
],
|
||||
source_module: 'link',
|
||||
destination_module: 'link',
|
||||
)]
|
||||
class LinkField extends D6LinkField {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFieldFormatterMap() {
|
||||
return [
|
||||
'link_default' => 'link',
|
||||
'link_title_plain' => 'link',
|
||||
'link_host' => 'link',
|
||||
'link_url' => 'link',
|
||||
'link_plain' => 'link',
|
||||
'link_absolute' => 'link',
|
||||
'link_domain' => 'link',
|
||||
'link_no_protocol' => 'link',
|
||||
'link_short' => 'link',
|
||||
'link_label' => 'link',
|
||||
'link_separate' => 'link_separate',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFieldWidgetMap() {
|
||||
// By default, use the plugin ID for the widget types.
|
||||
return ['link_field' => 'link_default'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function alterFieldInstanceMigration(MigrationInterface $migration) {
|
||||
$process = [
|
||||
'plugin' => 'static_map',
|
||||
'source' => 'settings/title',
|
||||
'bypass' => TRUE,
|
||||
'map' => [
|
||||
'disabled' => DRUPAL_DISABLED,
|
||||
'optional' => DRUPAL_OPTIONAL,
|
||||
'required' => DRUPAL_REQUIRED,
|
||||
],
|
||||
];
|
||||
$migration->mergeProcessOfProperty('settings/title', $process);
|
||||
}
|
||||
|
||||
}
|
||||
147
web/core/modules/link/src/Plugin/migrate/process/FieldLink.php
Normal file
147
web/core/modules/link/src/Plugin/migrate/process/FieldLink.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\link\Plugin\migrate\process;
|
||||
|
||||
use Drupal\migrate\Attribute\MigrateProcess;
|
||||
use Drupal\migrate\Plugin\MigrationInterface;
|
||||
use Drupal\migrate\MigrateExecutableInterface;
|
||||
use Drupal\migrate\ProcessPluginBase;
|
||||
use Drupal\migrate\Row;
|
||||
|
||||
/**
|
||||
* Transform a pre-Drupal 8 formatted link for use in Drupal 8.
|
||||
*
|
||||
* Previous to Drupal 8, URLs didn't need to have a URI scheme assigned. The
|
||||
* contrib link module would auto-prefix the URL with a URI scheme. A link in
|
||||
* Drupal 8 has more validation and external links must include the URI scheme.
|
||||
* All external URIs need to be converted to use a URI scheme.
|
||||
*
|
||||
* Available configuration keys
|
||||
* - uri_scheme: (optional) The URI scheme prefix to use for URLs without a
|
||||
* scheme. Defaults to 'http://', which was the default in Drupal 6 and
|
||||
* Drupal 7.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* Consider a link field migration, where you want to use https:// as the
|
||||
* prefix:
|
||||
*
|
||||
* @code
|
||||
* process:
|
||||
* field_link:
|
||||
* plugin: field_link
|
||||
* uri_scheme: 'https://'
|
||||
* source: field_link
|
||||
* @endcode
|
||||
*/
|
||||
#[MigrateProcess('field_link')]
|
||||
class FieldLink extends ProcessPluginBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
|
||||
$configuration += ['uri_scheme' => 'http://'];
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a Drupal 6/7 URI into a Drupal 8-compatible format.
|
||||
*
|
||||
* @param string $uri
|
||||
* The 'url' value from Drupal 6/7.
|
||||
*
|
||||
* @return string
|
||||
* The Drupal 8-compatible URI.
|
||||
*
|
||||
* @see \Drupal\link\Plugin\Field\FieldWidget\LinkWidget::getUserEnteredStringAsUri()
|
||||
*/
|
||||
protected function canonicalizeUri($uri) {
|
||||
// If the path starts with 2 slashes then it is always considered an
|
||||
// external URL without an explicit protocol part.
|
||||
// @todo Remove this when https://www.drupal.org/node/2744729 lands.
|
||||
if (str_starts_with($uri, '//')) {
|
||||
return $this->configuration['uri_scheme'] . ltrim($uri, '/');
|
||||
}
|
||||
|
||||
// If we already have a scheme, we're fine.
|
||||
if (parse_url($uri, PHP_URL_SCHEME)) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
// Empty URI and non-links are allowed.
|
||||
if (empty($uri) || in_array($uri, ['<nolink>', '<none>'])) {
|
||||
return 'route:<nolink>';
|
||||
}
|
||||
|
||||
// Remove the <front> component of the URL.
|
||||
if (str_starts_with($uri, '<front>')) {
|
||||
$uri = substr($uri, strlen('<front>'));
|
||||
}
|
||||
else {
|
||||
// List of unicode-encoded characters that were allowed in URLs,
|
||||
// according to link module in Drupal 7. Every character between ¿
|
||||
// and ÿ (except × × and ÷ ÷) with the addition of
|
||||
// Œ, œ and Ÿ.
|
||||
// @see https://git.drupalcode.org/project/link/blob/7.x-1.5-beta2/link.module#L1382
|
||||
// cSpell:disable-next-line
|
||||
$link_i_chars = '¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿŒœŸ';
|
||||
|
||||
// Pattern specific to internal links.
|
||||
$internal_pattern = "/^(?:[a-z0-9" . $link_i_chars . "_\-+\[\] ]+)";
|
||||
|
||||
$directories = "(?:\/[a-z0-9" . $link_i_chars . "_\-\.~+%=&,$'#!():;*@\[\]]*)*";
|
||||
// Yes, four backslashes == a single backslash.
|
||||
$query = "(?:\/?\?([?a-z0-9" . $link_i_chars . "+_|\-\.~\/\\\\%=&,$'():;*@\[\]{} ]*))";
|
||||
$anchor = "(?:#[a-z0-9" . $link_i_chars . "_\-\.~+%=&,$'():;*@\[\]\/\?]*)";
|
||||
|
||||
// The rest of the path for a standard URL.
|
||||
$end = $directories . '?' . $query . '?' . $anchor . '?$/i';
|
||||
|
||||
if (!preg_match($internal_pattern . $end, $uri)) {
|
||||
$link_domains = '[a-z][a-z0-9-]{1,62}';
|
||||
|
||||
// Starting a parenthesis group with (?: means that it is grouped, but
|
||||
// is not captured
|
||||
$authentication = "(?:(?:(?:[\w\.\-\+!$&'\(\)*\+,;=" . $link_i_chars . "]|%[0-9a-f]{2})+(?::(?:[\w" . $link_i_chars . "\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})*)?)?@)";
|
||||
$domain = '(?:(?:[a-z0-9' . $link_i_chars . ']([a-z0-9' . $link_i_chars . '\-_\[\]])*)(\.(([a-z0-9' . $link_i_chars . '\-_\[\]])+\.)*(' . $link_domains . '|[a-z]{2}))?)';
|
||||
$ipv4 = '(?:[0-9]{1,3}(\.[0-9]{1,3}){3})';
|
||||
$ipv6 = '(?:[0-9a-fA-F]{1,4}(\:[0-9a-fA-F]{1,4}){7})';
|
||||
$port = '(?::([0-9]{1,5}))';
|
||||
|
||||
// Pattern specific to external links.
|
||||
$external_pattern = '/^' . $authentication . '?(' . $domain . '|' . $ipv4 . '|' . $ipv6 . ' |localhost)' . $port . '?';
|
||||
if (preg_match($external_pattern . $end, $uri)) {
|
||||
return $this->configuration['uri_scheme'] . $uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the internal: scheme and ensure a leading slash.
|
||||
return 'internal:/' . ltrim($uri, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
|
||||
$attributes = unserialize($value['attributes']);
|
||||
// Drupal 6/7 link attributes might be double serialized.
|
||||
if (!is_array($attributes)) {
|
||||
$attributes = unserialize($attributes);
|
||||
}
|
||||
|
||||
// In rare cases Drupal 6/7 link attributes are triple serialized. To avoid
|
||||
// further problems with them we set them to an empty array in this case.
|
||||
if (!is_array($attributes)) {
|
||||
$attributes = [];
|
||||
}
|
||||
|
||||
// Massage the values into the correct form for the link.
|
||||
$route['uri'] = $this->canonicalizeUri($value['url']);
|
||||
$route['options']['attributes'] = $attributes;
|
||||
$route['title'] = $value['title'];
|
||||
return $route;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user