diff --git a/src/Processor/FieldsProcessorPluginBase.php b/src/Processor/FieldsProcessorPluginBase.php index c87e5c0..340a4ab 100644 --- a/src/Processor/FieldsProcessorPluginBase.php +++ b/src/Processor/FieldsProcessorPluginBase.php @@ -7,6 +7,7 @@ namespace Drupal\search_api\Processor; +use Drupal\Component\Utility\Html; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\search_api\Item\FieldInterface; @@ -54,7 +55,7 @@ abstract class FieldsProcessorPluginBase extends ProcessorPluginBase { } foreach ($fields as $name => $field) { if ($this->testType($field->getType())) { - $field_options[$name] = $field->getPrefixedLabel(); + $field_options[$name] = Html::escape($field->getPrefixedLabel()); if (!isset($this->configuration['fields']) && $this->testField($name, $field)) { $default_fields[$name] = $name; } diff --git a/src/Tests/IntegrationTest.php b/src/Tests/IntegrationTest.php index 874991d..b6bd6ac 100644 --- a/src/Tests/IntegrationTest.php +++ b/src/Tests/IntegrationTest.php @@ -7,6 +7,7 @@ namespace Drupal\search_api\Tests; +use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Unicode; use Drupal\search_api\Entity\Index; use Drupal\search_api\Entity\Server; @@ -43,6 +44,16 @@ class IntegrationTest extends WebTestBase { /** * {@inheritdoc} */ + public static $modules = array( + 'node', + 'search_api', + 'search_api_test_backend', + 'field_ui', + ); + + /** + * {@inheritdoc} + */ public function setUp() { parent::setUp(); $this->indexStorage = \Drupal::entityManager()->getStorage('search_api_index'); @@ -72,6 +83,8 @@ class IntegrationTest extends WebTestBase { $this->createIndex(); $this->checkContentEntityTracking(); + $this->checkFieldLabels(); + $this->addFieldsToIndex(); $this->addAdditionalFieldsToIndex(); $this->removeFieldsFromIndex(); @@ -89,10 +102,67 @@ class IntegrationTest extends WebTestBase { } /** + * Test that field labels are not double escaped nor not escaped at all. + */ + protected function checkFieldLabels() { + $permissions = array( + 'administer search_api', + 'access administration pages', + 'administer content types', + 'administer node fields' + ); + $this->drupalLogin($this->createUser($permissions)); + + $content_type_name = '&%@Content()_='; + + // Add a new content type with funky chars. + $edit = array( + 'name' => $content_type_name, + 'type' => '_content_' + ); + $this->drupalGet('admin/structure/types/add'); + $this->assertResponse(200); + $this->drupalPostForm(NULL, $edit, $this->t('Save and manage fields')); + + $field_name = '^6%{[*>.<"field'; + + // Add a field to that content type with funky chars. + $edit = array( + 'new_storage_type' => 'string', + 'label' => $field_name, + 'field_name' => '_field_' + ); + $this->drupalGet('admin/structure/types/manage/_content_/fields/add-field'); + $this->assertResponse(200); + $this->drupalPostForm(NULL, $edit, $this->t('Save and continue')); + $this->drupalPostForm(NULL, array(), $this->t('Save field settings')); + + $edit = array('fields[entity:node/field__field_][indexed]' => 1); + $this->drupalGet($this->getIndexPath('fields')); + $this->drupalPostForm(NULL, $edit, $this->t('Save changes')); + $this->assertHtmlEscaped($field_name); + + $edit = array( + 'datasource_configs[entity:node][default]' => 1, + ); + $this->drupalGet($this->getIndexPath('edit')); + $this->assertHtmlEscaped($content_type_name); + $this->drupalPostForm(NULL, $edit, $this->t('Save')); + + $edit = array('status[ignore_character]' => 1); + $this->drupalGet($this->getIndexPath('processors')); + $this->drupalPostForm(NULL, $edit, $this->t('Save')); + $this->assertHtmlEscaped($content_type_name); + $this->assertHtmlEscaped($field_name); + } + + /** * Tests creating a search server via the UI. */ - protected function createServer() { - $this->serverId = Unicode::strtolower($this->randomMachineName()); + protected function createServer($server_id = '_test_server') { + $this->serverId = $server_id; + $server_name = 'Search API &{}<>! Server'; + $server_description = 'A >server< used for testing &.'; $settings_path = $this->urlGenerator->generateFromRoute('entity.search_api_server.add_form', array(), array('absolute' => TRUE)); $this->drupalGet($settings_path); @@ -109,19 +179,19 @@ class IntegrationTest extends WebTestBase { $this->assertText($this->t('@name field is required.', array('@name' => $this->t('Server name')))); $edit = array( - 'name' => 'Search API test server', + 'name' => $server_name, 'status' => 1, - 'description' => 'A server used for testing.', + 'description' => $server_description, 'backend' => 'search_api_test_backend', ); $this->drupalPostForm($settings_path, $edit, $this->t('Save')); $this->assertText($this->t('@name field is required.', array('@name' => $this->t('Machine-readable name')))); $edit = array( - 'name' => 'Search API test server', + 'name' => $server_name, 'id' => $this->serverId, 'status' => 1, - 'description' => 'A server used for testing.', + 'description' => $server_description, 'backend' => 'search_api_test_backend', ); @@ -129,6 +199,12 @@ class IntegrationTest extends WebTestBase { $this->assertText($this->t('The server was successfully saved.')); $this->assertUrl('admin/config/search/search-api/server/' . $this->serverId, array(), 'Correct redirect to server page.'); + $this->assertHtmlEscaped($server_name); + $this->assertHtmlEscaped($server_description); + + $this->drupalGet('admin/config/search/search-api'); + $this->assertHtmlEscaped($server_name); + $this->assertHtmlEscaped($server_description); } /** @@ -136,12 +212,15 @@ class IntegrationTest extends WebTestBase { */ protected function createIndex() { $settings_path = $this->urlGenerator->generateFromRoute('entity.search_api_index.add_form', array(), array('absolute' => TRUE)); + $this->indexId = 'test_index'; + $index_description = 'An >index< used for &! tęsting.'; + $index_name = 'Search >API< test &!^* index'; $this->drupalGet($settings_path); $this->assertResponse(200); $edit = array( 'status' => 1, - 'description' => 'An index used for testing.', + 'description' => $index_description, ); $this->drupalPostForm(NULL, $edit, $this->t('Save')); @@ -149,13 +228,12 @@ class IntegrationTest extends WebTestBase { $this->assertText($this->t('@name field is required.', array('@name' => $this->t('Machine-readable name')))); $this->assertText($this->t('@name field is required.', array('@name' => $this->t('Data sources')))); - $this->indexId = 'test_index'; $edit = array( - 'name' => 'Search API test index', + 'name' => $index_name, 'id' => $this->indexId, 'status' => 1, - 'description' => 'An index used for testing.', + 'description' => $index_description, 'server' => $this->serverId, 'datasources[]' => array('entity:node'), ); @@ -164,6 +242,10 @@ class IntegrationTest extends WebTestBase { $this->assertText($this->t('The index was successfully saved.')); $this->assertUrl($this->getIndexPath(), array(), 'Correct redirect to index page.'); + $this->assertHtmlEscaped($index_name); + + $this->drupalGet($this->getIndexPath('edit')); + $this->assertHtmlEscaped($index_name); $this->indexStorage->resetCache(array($this->indexId)); /** @var $index \Drupal\search_api\IndexInterface */ @@ -192,6 +274,10 @@ class IntegrationTest extends WebTestBase { $this->indexStorage->resetCache(array($index2_id)); $index = $this->indexStorage->load($index2_id); $this->assertUrl($index->urlInfo('fields'), array(), 'Correct redirect to index fields page.'); + + $this->drupalGet('admin/config/search/search-api'); + $this->assertHtmlEscaped($index_name); + $this->assertHtmlEscaped($index_description); } /** @@ -593,7 +679,7 @@ class IntegrationTest extends WebTestBase { $this->assertEqual($remaining_items, 0, 'All items have been successfully indexed.'); // Create a second search server. - $this->createServer(); + $this->createServer('test_server_2'); // Change the index's server to the new one. $settings_path = $this->getIndexPath('edit'); @@ -652,4 +738,19 @@ class IntegrationTest extends WebTestBase { return $path; } + /** + * Ensures that all occurrences of the string are properly escaped. + * + * This makes sure that the string is only mentioned in an escaped version and + * is never double escaped. + * + * @param string $string + * The raw string to check for. + */ + protected function assertHtmlEscaped($string) { + $this->assertRaw(Html::escape($string)); + $this->assertNoRaw(Html::escape(Html::escape($string))); + $this->assertNoRaw($string); + } + }