diff --git a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
index c8709c5689..5622ae2496 100644
--- a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
+++ b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
@@ -205,10 +205,12 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
}
}
else {
+ $options = $url->getOptions();
+ unset($options['query']);
$element[$delta] = [
'#type' => 'link',
'#title' => $link_title,
- '#options' => $url->getOptions(),
+ '#options' => $options,
];
$element[$delta]['#url'] = $url;
diff --git a/core/modules/link/tests/src/Functional/LinkFieldTest.php b/core/modules/link/tests/src/Functional/LinkFieldTest.php
index c6d2be3bd2..ba5d63a8d3 100644
--- a/core/modules/link/tests/src/Functional/LinkFieldTest.php
+++ b/core/modules/link/tests/src/Functional/LinkFieldTest.php
@@ -179,6 +179,9 @@ protected function doTestURLValidation() {
'entity:user/999999' => 'entity:user/999999',
];
+ // Add to array url with complex query parameters.
+ $valid_internal_entries += $this->getUrlWithComplexQueryInputList();
+
// Define some invalid URLs.
$validation_error_1 = "The path '@link_path' is invalid.";
$validation_error_2 = 'Manually entered paths should start with one of the following characters: / ? #';
@@ -462,7 +465,7 @@ protected function doTestLinkFormatter() {
// Not using generatePermutations(), since that leads to 32 cases, which
// would not test actual link field formatter functionality but rather
// the link generator and options/attributes. Only 'url_plain' has a
- // dependency on 'url_only', so we have a total of ~10 cases.
+ // dependency on 'url_only'.
$options = [
'trim_length' => [NULL, 6],
'rel' => [NULL, 'nofollow'],
@@ -545,6 +548,188 @@ protected function doTestLinkFormatter() {
}
}
+ /**
+ * Tests the default 'link' formatter with complex query parameters.
+ */
+ public function testLinkFormatterQueryParametersDuplication() {
+ $test_urls = $this->getUrlWithComplexQuery();
+ $field_name = mb_strtolower($this->randomMachineName());
+ // Create a field with settings to validate.
+ $this->fieldStorage = FieldStorageConfig::create([
+ 'field_name' => $field_name,
+ 'entity_type' => 'entity_test',
+ 'type' => 'link',
+ 'cardinality' => count($test_urls),
+ ]);
+ $this->fieldStorage->save();
+ FieldConfig::create([
+ 'field_storage' => $this->fieldStorage,
+ 'label' => 'Read more about this entity',
+ 'bundle' => 'entity_test',
+ 'settings' => [
+ 'title' => DRUPAL_OPTIONAL,
+ 'link_type' => LinkItemInterface::LINK_GENERIC,
+ ],
+ ])->save();
+ /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
+ $display_repository = \Drupal::service('entity_display.repository');
+ $display_repository->getFormDisplay('entity_test', 'entity_test', 'default')
+ ->setComponent($field_name, [
+ 'type' => 'link_default',
+ ])
+ ->save();
+ $display_options = [
+ 'type' => 'link',
+ 'label' => 'hidden',
+ ];
+ $display_repository->getViewDisplay('entity_test', 'entity_test', 'full')
+ ->setComponent($field_name, $display_options)
+ ->save();
+
+ // Create an entity with link field values provided
+ // by $this->getUrlWithComplexQuery().
+ $this->drupalGet('entity_test/add');
+ $edit = [];
+ foreach ($test_urls as $key => $test_url) {
+ $edit["{$field_name}[$key][uri]"] = $test_url['inputByUser'];
+ $edit["{$field_name}[$key][title]"] = $test_url['inputByUser'];
+ }
+ // Assert label is shown.
+ $this->assertSession()->pageTextContains('Read more about this entity');
+ $this->submitForm($edit, 'Save');
+ preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
+ $id = $match[1];
+ $this->assertSession()->pageTextContains("entity_test $id has been created.");
+
+ // Verify that the link is output according to the formatter settings.
+ // Not using generatePermutations(), since that leads to 32 cases, which
+ // would not test actual link field formatter functionality but rather
+ // the link generator and options/attributes. Only 'url_plain' has a
+ // dependency on 'url_only', so we have a total of ~10 cases.
+ $options = [
+ 'trim_length' => [NULL, 6],
+ 'rel' => [NULL, 'nofollow'],
+ 'target' => [NULL, '_blank'],
+ 'url_only' => [
+ ['url_only' => FALSE],
+ ['url_only' => FALSE, 'url_plain' => TRUE],
+ ['url_only' => TRUE],
+ ['url_only' => TRUE, 'url_plain' => TRUE],
+ ],
+ ];
+ foreach ($options as $setting => $values) {
+ foreach ($values as $new_value) {
+ // Update the field formatter settings.
+ if (!is_array($new_value)) {
+ $display_options['settings'] = [$setting => $new_value];
+ }
+ else {
+ $display_options['settings'] = $new_value;
+ }
+ $display_repository->getViewDisplay('entity_test', 'entity_test', 'full')
+ ->setComponent($field_name, $display_options)
+ ->save();
+
+ $output = $this->renderTestEntity($id);
+ foreach ($test_urls as $test_url) {
+ $url = $test_url['renderedHref'];
+ $title = $test_url['inputByUser'];
+ switch ($setting) {
+ case 'trim_length':
+ $title = isset($new_value) ? Unicode::truncate($title, $new_value, FALSE, TRUE) : $title;
+ $this->assertStringContainsString('' . Html::escape($title) . '', $output);
+ break;
+
+ case 'rel':
+ $rel = isset($new_value) ? ' rel="' . $new_value . '"' : '';
+ $this->assertStringContainsString('' . Html::escape($title) . '', $output);
+ break;
+
+ case 'target':
+ $target = isset($new_value) ? ' target="' . $new_value . '"' : '';
+ $this->assertStringContainsString('' . Html::escape($title) . '', $output);
+ break;
+
+ case 'url_only':
+ // In this case, $new_value is an array.
+ if (!$new_value['url_only']) {
+ $this->assertStringContainsString('' . Html::escape($title) . '', $output);
+ }
+ else {
+ if (empty($new_value['url_plain'])) {
+ $this->assertStringContainsString('' . $url . '', $output);
+ }
+ else {
+ $this->assertStringNotContainsString('' . $url . '', $output);
+ $this->assertStringContainsString($url, $output);
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Get array of url with complex query parameters for render check.
+ *
+ * @return array
+ * The URLs to test.
+ */
+ protected function getUrlWithComplexQuery() {
+ $test_urls = [
+ [
+ 'inputByUser' => '?a[]=1&a[]=2',
+ 'renderedHref' => '?a%5B0%5D=1&a%5B1%5D=2',
+ ],
+ [
+ 'inputByUser' => '?b[0]=1&b[1]=2',
+ 'renderedHref' => '?b%5B0%5D=1&b%5B1%5D=2',
+ ],
+ // UrlHelper::buildQuery will change order of params.
+ [
+ 'inputByUser' => '?c[]=1&d=3&c[]=2',
+ 'renderedHref' => '?c%5B0%5D=1&c%5B1%5D=2&d=3',
+ ],
+ [
+ 'inputByUser' => '?e[f][g]=h',
+ 'renderedHref' => '?e%5Bf%5D%5Bg%5D=h',
+ ],
+ [
+ 'inputByUser' => '?i[j[k]]=l',
+ 'renderedHref' => '?i%5Bj%5Bk%5D=l',
+ ],
+
+ // Query string replace value.
+ [
+ 'inputByUser' => '?x=1&x=2',
+ 'renderedHref' => '?x=2',
+ ],
+ [
+ 'inputByUser' => '?z[0]=1&z[0]=2',
+ 'renderedHref' => '?z%5B0%5D=2',
+ ],
+ ];
+ return $test_urls;
+ }
+
+ /**
+ * Get list of url with complex query parameters for input check.
+ *
+ * @return array
+ * The URLs with complex query parameters.
+ */
+ protected function getUrlWithComplexQueryInputList() {
+ $test_urls = $this->getUrlWithComplexQuery();
+ $list_urls = [];
+ foreach ($test_urls as $test_url) {
+ $list_urls[$test_url['inputByUser']] = Html::escape($test_url['inputByUser']);
+ }
+
+ return $list_urls;
+ }
+
/**
* Tests the 'link_separate' formatter.
*