From cfa9fbbbef5645c8d831ffaaf7666b9d361886e2 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Tue, 15 Oct 2019 09:21:48 +1000 Subject: [PATCH] Issue #2174633 by colan, mikeker, Lendude, Pancho, Jo Fitzgerald, tim.plunkett, andypost, anya_m, botanic_spark, amateescu, realityloop, tvhung, lokapujya, jhedstrom, rensingh99, dww, Munavijayalakshmi, david.gil, jlbellido, visabhishek, druprad, le72, larowlan, kenton.r, jonathanshaw, Damien Tournoud, mpp, alexpott, dawehner, nicholas.alipaz, TrevorBradley, jp.stacey, Berdir: View output is not used for entityreference options --- .../Entity/Element/EntityAutocomplete.php | 2 +- .../EntityReference/Views/SelectionTest.php | 119 ++++++++++++++++++ .../EntityReference/Views/SelectionTest.php | 55 +++++--- .../views.view.test_entity_reference.yml | 112 +++++++++++++++-- .../ViewsSelection.php | 109 ++++++++++++---- 5 files changed, 353 insertions(+), 44 deletions(-) create mode 100644 core/modules/field/tests/src/Functional/EntityReference/Views/SelectionTest.php diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index 0691239dfa4..10421abaae2 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -296,7 +296,7 @@ class EntityAutocomplete extends Textfield { $multiples[] = $name . ' (' . $id . ')'; } $params['@id'] = $id; - $form_state->setError($element, t('Multiple entities match this reference; "%multiple". Specify the one you want by appending the id in parentheses, like "@value (@id)".', ['%multiple' => implode('", "', $multiples)] + $params)); + $form_state->setError($element, t('Multiple entities match this reference; "%multiple". Specify the one you want by appending the id in parentheses, like "@value (@id)".', ['%multiple' => strip_tags(implode('", "', $multiples))] + $params)); } else { // Take the one and only matching entity. diff --git a/core/modules/field/tests/src/Functional/EntityReference/Views/SelectionTest.php b/core/modules/field/tests/src/Functional/EntityReference/Views/SelectionTest.php new file mode 100644 index 00000000000..16a18d96c11 --- /dev/null +++ b/core/modules/field/tests/src/Functional/EntityReference/Views/SelectionTest.php @@ -0,0 +1,119 @@ +drupalCreateContentType()->id(); + $type2 = $this->drupalCreateContentType()->id(); + // Add some characters that should be escaped but not double escaped. + $node1 = $this->drupalCreateNode(['type' => $type1, 'title' => 'Test first node &<>']); + $node2 = $this->drupalCreateNode(['type' => $type1, 'title' => 'Test second node &&&']); + $node3 = $this->drupalCreateNode(['type' => $type2, 'title' => 'Test third node ']); + + foreach ([$node1, $node2, $node3] as $node) { + $this->nodes[$node->id()] = $node; + } + + // Create an entity reference field. + $handler_settings = [ + 'view' => [ + 'view_name' => 'test_entity_reference', + 'display_name' => 'entity_reference_1', + ], + ]; + $this->handlerSettings = $handler_settings; + $this->createEntityReferenceField('entity_test', 'test_bundle', 'test_field', $this->randomString(), 'node', 'views', $handler_settings); + } + + /** + * Tests that the Views selection handles the views output properly. + */ + public function testAutocompleteOutput() { + // Reset any internal static caching. + \Drupal::service('entity_type.manager')->getStorage('node')->resetCache(); + + $view = Views::getView('test_entity_reference'); + $view->setDisplay(); + + // Enable the display of the 'type' field so we can test that the output + // does not contain only the entity label. + $fields = $view->displayHandlers->get('entity_reference_1')->getOption('fields'); + $fields['type']['exclude'] = FALSE; + $view->displayHandlers->get('entity_reference_1')->setOption('fields', $fields); + $view->save(); + + // Prepare the selection settings key needed by the entity reference + // autocomplete route. + $target_type = 'node'; + $selection_handler = 'views'; + $selection_settings = $this->handlerSettings; + $selection_settings_key = Crypt::hmacBase64(serialize($selection_settings) . $target_type . $selection_handler, Settings::getHashSalt()); + \Drupal::keyValue('entity_autocomplete')->set($selection_settings_key, $selection_settings); + + $result = Json::decode($this->drupalGet('entity_reference_autocomplete/' . $target_type . '/' . $selection_handler . '/' . $selection_settings_key, ['query' => ['q' => 't']])); + + $expected = [ + 0 => [ + 'value' => $this->nodes[1]->bundle() . ': ' . $this->nodes[1]->label() . ' (' . $this->nodes[1]->id() . ')', + 'label' => '' . $this->nodes[1]->bundle() . ': ' . Html::escape($this->nodes[1]->label()) . '', + ], + 1 => [ + 'value' => $this->nodes[2]->bundle() . ': ' . $this->nodes[2]->label() . ' (' . $this->nodes[2]->id() . ')', + 'label' => '' . $this->nodes[2]->bundle() . ': ' . Html::escape($this->nodes[2]->label()) . '', + ], + 2 => [ + 'value' => $this->nodes[3]->bundle() . ': ' . $this->nodes[3]->label() . ' (' . $this->nodes[3]->id() . ')', + 'label' => '' . $this->nodes[3]->bundle() . ': ' . Html::escape($this->nodes[3]->label()) . '', + ], + ]; + $this->assertEqual($result, $expected, 'The autocomplete result of the Views entity reference selection handler contains the proper output.'); + } + +} diff --git a/core/modules/field/tests/src/Kernel/EntityReference/Views/SelectionTest.php b/core/modules/field/tests/src/Kernel/EntityReference/Views/SelectionTest.php index 5a41448201e..4ce58956281 100644 --- a/core/modules/field/tests/src/Kernel/EntityReference/Views/SelectionTest.php +++ b/core/modules/field/tests/src/Kernel/EntityReference/Views/SelectionTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\field\Kernel\EntityReference\Views; +use Drupal\Component\Utility\Html; use Drupal\field\Entity\FieldConfig; use Drupal\KernelTests\KernelTestBase; use Drupal\node\Entity\NodeType; @@ -40,6 +41,13 @@ class SelectionTest extends KernelTestBase { */ protected $nodes = []; + /** + * The selection handler. + * + * @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface + */ + protected $selectionHandler; + /** * {@inheritdoc} */ @@ -58,7 +66,7 @@ class SelectionTest extends KernelTestBase { $node3 = $this->createNode(); foreach ([$node1, $node2, $node3] as $node) { - $this->nodes[$node->bundle()][$node->id()] = $node->label(); + $this->nodes[$node->id()] = $node; } // Create an entity reference field. @@ -69,18 +77,16 @@ class SelectionTest extends KernelTestBase { ], ]; $this->createEntityReferenceField('entity_test', 'test_bundle', 'test_field', $this->randomString(), 'node', 'views', $handler_settings); + $field_config = FieldConfig::loadByName('entity_test', 'test_bundle', 'test_field'); + $this->selectionHandler = $this->container->get('plugin.manager.entity_reference_selection')->getSelectionHandler($field_config); } /** * Tests the selection handler. */ public function testSelectionHandler() { - $field_config = FieldConfig::loadByName('entity_test', 'test_bundle', 'test_field'); - $selection_handler = $this->container->get('plugin.manager.entity_reference_selection')->getSelectionHandler($field_config); - // Tests the selection handler. - $result = $selection_handler->getReferenceableEntities(); - $this->assertResults($result); + $this->assertResults($this->selectionHandler->getReferenceableEntities()); // Add a relationship to the view. $view = Views::getView('test_entity_reference'); @@ -110,8 +116,33 @@ class SelectionTest extends KernelTestBase { $view->save(); // Tests the selection handler with a relationship. - $result = $selection_handler->getReferenceableEntities(); - $this->assertResults($result); + $this->assertResults($this->selectionHandler->getReferenceableEntities()); + } + + /** + * Tests the anchor tag stripping. + * + * Unstripped results based on the data above will result in output like so: + * ...Test first node... + * ...Test second node... + * ...Test third node... + * If we expect our output to not have the tags, and this matches what's + * produced by the tag-stripping method, we'll know that it's working. + */ + public function testAnchorTagStripping() { + $filtered_rendered_results_formatted = []; + foreach ($this->selectionHandler->getReferenceableEntities() as $subresults) { + $filtered_rendered_results_formatted += $subresults; + } + + // Note the missing tags. + $expected = [ + 1 => '' . Html::escape($this->nodes[1]->label()) . '', + 2 => '' . Html::escape($this->nodes[2]->label()) . '', + 3 => '' . Html::escape($this->nodes[3]->label()) . '', + ]; + + $this->assertEqual($filtered_rendered_results_formatted, $expected, 'Anchor tag stripping has failed.'); } /** @@ -121,16 +152,12 @@ class SelectionTest extends KernelTestBase { * Query results keyed by node type and nid. */ protected function assertResults(array $result) { - $success = FALSE; foreach ($result as $node_type => $values) { foreach ($values as $nid => $label) { - if (!$success = $this->nodes[$node_type][$nid] == trim(strip_tags($label))) { - // There was some error, so break. - break; - } + $this->assertSame($node_type, $this->nodes[$nid]->bundle()); + $this->assertSame(trim(strip_tags($label)), Html::escape($this->nodes[$nid]->label())); } } - $this->assertTrue($success, 'Views selection handler returned expected values.'); } } diff --git a/core/modules/system/tests/modules/entity_reference_test/config/install/views.view.test_entity_reference.yml b/core/modules/system/tests/modules/entity_reference_test/config/install/views.view.test_entity_reference.yml index 1bcb19c455b..334c3d61228 100644 --- a/core/modules/system/tests/modules/entity_reference_test/config/install/views.view.test_entity_reference.yml +++ b/core/modules/system/tests/modules/entity_reference_test/config/install/views.view.test_entity_reference.yml @@ -2,12 +2,11 @@ langcode: en status: true dependencies: module: - - entity_reference_test - node - user id: test_entity_reference label: 'Entity reference' -module: entity_reference_test +module: views description: '' tag: '' base_table: node_field_data @@ -35,6 +34,71 @@ display: row: type: fields fields: + type: + id: type + table: node_field_data + field: type + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: true + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_label + settings: + link: false + group_column: target_id + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: type + plugin_id: field title: id: title table: node_field_data @@ -99,14 +163,30 @@ display: entity_type: node entity_field: status sorts: - created: - id: created + nid: + id: nid table: node_field_data - field: created - order: DESC - plugin_id: date + field: nid + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' entity_type: node - entity_field: created + entity_field: nid + plugin_id: standard + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } entity_reference_1: display_plugin: entity_reference id: entity_reference_1 @@ -123,3 +203,19 @@ display: type: none options: offset: 0 + display_extenders: { } + row: + type: entity_reference + options: + default_field_elements: true + inline: { } + separator: ': ' + hide_empty: false + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php b/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php index 6def8d8fefb..0f75ebe4306 100644 --- a/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php +++ b/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php @@ -2,16 +2,20 @@ namespace Drupal\views\Plugin\EntityReferenceSelection; +use Drupal\Component\Utility\Xss; use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait; use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginBase; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; +use Drupal\views\Render\ViewsRenderPipelineMarkup; use Drupal\views\Views; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Plugin implementation of the 'selection' entity_reference. @@ -25,7 +29,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ class ViewsSelection extends SelectionPluginBase implements ContainerFactoryPluginInterface { use DeprecatedServicePropertyTrait; - + use StringTranslationTrait; /** * {@inheritdoc} */ @@ -59,6 +63,13 @@ class ViewsSelection extends SelectionPluginBase implements ContainerFactoryPlug */ protected $currentUser; + /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + /** * Constructs a new ViewsSelection object. * @@ -74,13 +85,21 @@ class ViewsSelection extends SelectionPluginBase implements ContainerFactoryPlug * The module handler service. * @param \Drupal\Core\Session\AccountInterface $current_user * The current user. + * @param \Drupal\Core\Render\RendererInterface|null $renderer + * The renderer. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user, RendererInterface $renderer = NULL) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->entityTypeManager = $entity_type_manager; $this->moduleHandler = $module_handler; $this->currentUser = $current_user; + + if (!$renderer) { + @trigger_error('Calling ' . __METHOD__ . ' without the $renderer argument is deprecated in drupal:8.8.0 and is required in drupal:9.0.0. See https://www.drupal.org/node/2791359', E_USER_DEPRECATED); + $renderer = \Drupal::service('renderer'); + } + $this->renderer = $renderer; } /** @@ -93,7 +112,8 @@ class ViewsSelection extends SelectionPluginBase implements ContainerFactoryPlug $plugin_definition, $container->get('entity_type.manager'), $container->get('module_handler'), - $container->get('current_user') + $container->get('current_user'), + $container->get('renderer') ); } @@ -199,7 +219,7 @@ class ViewsSelection extends SelectionPluginBase implements ContainerFactoryPlug // Check that the view is valid and the display still exists. $this->view = Views::getView($view_name); if (!$this->view || !$this->view->access($display_name)) { - \Drupal::messenger()->addWarning(t('The reference view %view_name cannot be found.', ['%view_name' => $view_name])); + \Drupal::messenger()->addWarning($this->t('The reference view %view_name cannot be found.', ['%view_name' => $view_name])); return FALSE; } $this->view->setDisplay($display_name); @@ -219,22 +239,68 @@ class ViewsSelection extends SelectionPluginBase implements ContainerFactoryPlug * {@inheritdoc} */ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) { + $entities = []; + if ($display_execution_results = $this->getDisplayExecutionResults($match, $match_operator, $limit)) { + $entities = $this->stripAdminAndAnchorTagsFromResults($display_execution_results); + } + return $entities; + } + + /** + * Fetches the results of executing the display. + * + * @param string|null $match + * (Optional) Text to match the label against. Defaults to NULL. + * @param string $match_operator + * (Optional) The operation the matching should be done with. Defaults + * to "CONTAINS". + * @param int $limit + * Limit the query to a given number of items. Defaults to 0, which + * indicates no limiting. + * @param array|null $ids + * Array of entity IDs. Defaults to NULL. + * + * @return array + * The results. + */ + protected function getDisplayExecutionResults(string $match = NULL, string $match_operator = 'CONTAINS', int $limit = 0, array $ids = NULL) { $display_name = $this->getConfiguration()['view']['display_name']; $arguments = $this->getConfiguration()['view']['arguments']; - $result = []; - if ($this->initializeView($match, $match_operator, $limit)) { - // Get the results. - $result = $this->view->executeDisplay($display_name, $arguments); + $results = []; + if ($this->initializeView($match, $match_operator, $limit, $ids)) { + $results = $this->view->executeDisplay($display_name, $arguments); + } + return $results; + } + + /** + * Strips all admin and anchor tags from a result list. + * + * These results are usually displayed in an autocomplete field, which is + * surrounded by anchor tags. Most tags are allowed inside anchor tags, except + * for other anchor tags. + * + * @param array $results + * The result list. + * + * @return array + * The provided result list with anchor tags removed. + */ + protected function stripAdminAndAnchorTagsFromResults(array $results) { + $allowed_tags = Xss::getAdminTagList(); + if (($key = array_search('a', $allowed_tags)) !== FALSE) { + unset($allowed_tags[$key]); } - $return = []; - if ($result) { - foreach ($this->view->result as $row) { - $entity = $row->_entity; - $return[$entity->bundle()][$entity->id()] = $entity->label(); - } + $stripped_results = []; + foreach ($results as $id => $row) { + $entity = $row['#row']->_entity; + $stripped_results[$entity->bundle()][$id] = ViewsRenderPipelineMarkup::create( + Xss::filter($this->renderer->renderPlain($row), $allowed_tags) + ); } - return $return; + + return $stripped_results; } /** @@ -249,12 +315,9 @@ class ViewsSelection extends SelectionPluginBase implements ContainerFactoryPlug * {@inheritdoc} */ public function validateReferenceableEntities(array $ids) { - $display_name = $this->getConfiguration()['view']['display_name']; - $arguments = $this->getConfiguration()['view']['arguments']; + $entities = $this->getDisplayExecutionResults(NULL, 'CONTAINS', 0, $ids); $result = []; - if ($this->initializeView(NULL, 'CONTAINS', 0, $ids)) { - // Get the results. - $entities = $this->view->executeDisplay($display_name, $arguments); + if ($entities) { $result = array_keys($entities); } return $result; @@ -285,7 +348,11 @@ class ViewsSelection extends SelectionPluginBase implements ContainerFactoryPlug $arguments = array_map('trim', explode(',', $arguments_string)); } - $value = ['view_name' => $view, 'display_name' => $display, 'arguments' => $arguments]; + $value = [ + 'view_name' => $view, + 'display_name' => $display, + 'arguments' => $arguments, + ]; $form_state->setValueForElement($element, $value); }