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. *