diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 009bb33..2c18b38 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -16,6 +16,7 @@ use Drupal\Component\Utility\Xss; use Drupal\Core\Config\Config; use Drupal\Core\Config\StorageException; +use Drupal\Core\Render\SafeString; use Drupal\Core\Template\Attribute; use Drupal\Core\Theme\ThemeSettings; use Drupal\Component\Utility\NestedArray; @@ -1066,6 +1067,73 @@ function template_preprocess_item_list(&$variables) { } /** + * Prepares variables for inline list templates. + * + * Default template: inline-list.html.twig. + * + * @param array $variables + * An associative array containing: + * - items: An array of items to be displayed in the list. Each item can be + * either a string or a render array. If #type, #theme, or #markup + * properties are not specified for child render arrays, they will be + * inherited from the parent list, allowing callers to specify larger + * nested lists without having to explicitly specify and repeat the + * render properties for all nested child lists. + * - separator: A string to separate list items. + * + * @see https://www.drupal.org/node/1842756 + */ +function template_preprocess_inline_list(&$variables) { + foreach ($variables['items'] as &$item) { + $attributes = array(); + // If the item value is an array, then it is a render array. + if (is_array($item)) { + // List items support attributes via the '#wrapper_attributes' property. + if (isset($item['#wrapper_attributes'])) { + $attributes = $item['#wrapper_attributes']; + } + // Determine whether there are any child elements in the item that are not + // fully-specified render arrays. If there are any, then the child + // elements present nested lists and we automatically inherit the render + // array properties of the current list to them. + foreach (Element::children($item) as $key) { + $child = &$item[$key]; + // If this child element does not specify how it can be rendered, then + // we need to inherit the render properties of the current list. + if (!isset($child['#type']) && !isset($child['#theme']) && !isset($child['#markup'])) { + // Since inline-list.html.twig supports both strings and render arrays + // as items, the items of the nested list may have been specified as + // the child elements of the nested list, instead of #items. For + // convenience, we automatically move them into #items. + if (!isset($child['#items'])) { + // This is the same condition as in + // \Drupal\Core\Render\Element::children(), which cannot be used + // here, since it triggers an error on string values. + foreach ($child as $child_key => $child_value) { + if ($child_key[0] !== '#') { + $child['#items'][$child_key] = $child_value; + unset($child[$child_key]); + } + } + } + // Lastly, inherit the original theme variables of the current list. + $child['#theme'] = $variables['theme_hook_original']; + } + } + } + + // Set the item's value and attributes for the template. + $item = array( + 'value' => $item, + 'attributes' => new Attribute($attributes), + ); + + // Since the separator may be user-specified, it must be sanitized. + $variables['separator'] = SafeString::create(Xss::filterAdmin($variables['separator'])); + } +} + +/** * Returns HTML for an indentation div; used for drag and drop tables. * * @param $variables @@ -1726,6 +1794,9 @@ function drupal_common_theme() { 'item_list' => array( 'variables' => array('items' => array(), 'title' => '', 'list_type' => 'ul', 'attributes' => array(), 'empty' => NULL, 'context' => array()), ), + 'inline_list' => array( + 'variables' => array('items' => array(), 'separator' => ', ', 'attributes' => array(), 'empty' => NULL, 'context' => array()), + ), 'feed_icon' => array( 'variables' => array('url' => NULL, 'title' => NULL), ), diff --git a/core/modules/system/system.module b/core/modules/system/system.module index d6d98c8..60aea45 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -256,6 +256,14 @@ function system_theme_suggestions_html(array $variables) { /** * Implements hook_theme_suggestions_HOOK(). */ +function system_theme_suggestions_item_list(array $variables) { + $types = ['ul', 'ol', 'inline']; + return theme_get_suggestions($types, 'item_list'); +} + +/** + * Implements hook_theme_suggestions_HOOK(). + */ function system_theme_suggestions_page(array $variables) { if (\Drupal::service('path.matcher')->isFrontPage()) { $path_args = ['']; diff --git a/core/modules/system/templates/inline-list.html.twig b/core/modules/system/templates/inline-list.html.twig new file mode 100644 index 0000000..5230eb7 --- /dev/null +++ b/core/modules/system/templates/inline-list.html.twig @@ -0,0 +1,29 @@ +{# +/** + * @file + * Default theme implementation for an inline list of items. + * + * Available variables: + * - items: A list of items. Each item contains: + * - attributes: HTML attributes to be applied to each list item. + * - value: The content of the list element. + * - separator: A string to separate list items. + * - list_type: The tag for list element ("ul" or "ol"). + * - attributes: HTML attributes to be applied to the list. + * - empty: A message to display when there are no items. Allowed value is a + * string or render array. + * + * @see template_preprocess_inline_list() + * + * @ingroup themeable + */ +#} +{% if items or empty -%} + {%- if items -%} + {%- for item in items -%} + {{ item.value }}{{ loop.last ? '' : separator }} + {%- endfor -%} + {%- else -%} + {{- empty -}} + {%- endif -%} +{%- endif %} diff --git a/core/modules/views_ui/src/Tests/DisplayPathTest.php b/core/modules/views_ui/src/Tests/DisplayPathTest.php index 004b9f0..62c1f2f 100644 --- a/core/modules/views_ui/src/Tests/DisplayPathTest.php +++ b/core/modules/views_ui/src/Tests/DisplayPathTest.php @@ -35,6 +35,7 @@ class DisplayPathTest extends UITestBase { public function testPathUI() { $this->doBasicPathUITest(); $this->doAdvancedPathsValidationTest(); + $this->doPathXssFilterTest(); } /** @@ -60,6 +61,29 @@ protected function doBasicPathUITest() { } /** + * Tests that View paths are properly filtered for XSS. + */ + public function doPathXssFilterTest() { + global $base_path; + $this->drupalGet('admin/structure/views/view/test_view'); + $this->drupalPostForm(NULL, array(), 'Add Page'); + $this->drupalPostForm('admin/structure/views/nojs/display/test_view/page_2/path', array('path' => 'malformed_path'), t('Apply')); + $this->drupalPostForm(NULL, array(), 'Add Page'); + $this->drupalPostForm('admin/structure/views/nojs/display/test_view/page_3/path', array('path' => ''), t('Apply')); + $this->drupalPostForm(NULL, array(), 'Add Page'); + $this->drupalPostForm('admin/structure/views/nojs/display/test_view/page_4/path', array('path' => ''), t('Apply')); + $this->drupalPostForm('admin/structure/views/view/test_view', array(), t('Save')); + $this->drupalGet('admin/structure/views'); + // The anchor text should be escaped. + $this->assertEscaped('/malformed_path'); + $this->assertEscaped('/'); + $this->assertEscaped('/'); + // Links should be url-encoded. + $this->assertRaw('/%3Cobject%3Emalformed_path%3C/object%3E'); + $this->assertRaw('/%3Cscript%3Ealert%28%22hello%22%29%3B%3C/script%3E'); + } + + /** * Tests a couple of invalid path patterns. */ protected function doAdvancedPathsValidationTest() { diff --git a/core/modules/views_ui/src/ViewListBuilder.php b/core/modules/views_ui/src/ViewListBuilder.php index 00be178..ba198a2 100644 --- a/core/modules/views_ui/src/ViewListBuilder.php +++ b/core/modules/views_ui/src/ViewListBuilder.php @@ -91,12 +91,6 @@ public function load() { */ public function buildRow(EntityInterface $view) { $row = parent::buildRow($view); - $display_paths = ''; - $separator = ''; - foreach ($this->getDisplayPaths($view) as $display_path) { - $display_paths .= $separator . SafeMarkup::escape($display_path); - $separator = ', '; - } return array( 'data' => array( 'view_name' => array( @@ -113,7 +107,12 @@ public function buildRow(EntityInterface $view) { 'class' => array('views-table-filter-text-source'), ), 'tag' => $view->get('tag'), - 'path' => SafeMarkup::set($display_paths), + 'path' => array( + 'data' => array( + '#theme' => 'inline_list', + '#items' => $this->getDisplayPaths($view), + ), + ), 'operations' => $row['operations'], ), 'title' => $this->t('Machine name: @name', array('@name' => $view->id())), diff --git a/core/modules/views_ui/tests/src/Unit/ViewListBuilderTest.php b/core/modules/views_ui/tests/src/Unit/ViewListBuilderTest.php index a42a879..d4bc2b6 100644 --- a/core/modules/views_ui/tests/src/Unit/ViewListBuilderTest.php +++ b/core/modules/views_ui/tests/src/Unit/ViewListBuilderTest.php @@ -89,7 +89,10 @@ public function testBuildRowEntityList() { ); $page_display->expects($this->any()) ->method('getPath') - ->will($this->returnValue('test_page')); + ->will($this->onConsecutiveCalls( + $this->returnValue('test_page'), + $this->returnValue('malformed_path'), + $this->returnValue(''))); $embed_display = $this->getMock('Drupal\views\Plugin\views\display\Embed', array('initDisplay'), array(array(), 'default', $display_manager->getDefinition('embed')) @@ -106,6 +109,16 @@ public function testBuildRowEntityList() { $values['display']['page_1']['display_plugin'] = 'page'; $values['display']['page_1']['display_options']['path'] = 'test_page'; + $values['display']['page_2']['id'] = 'page_2'; + $values['display']['page_2']['display_title'] = 'Page 2'; + $values['display']['page_2']['display_plugin'] = 'page'; + $values['display']['page_2']['display_options']['path'] = 'malformed_path'; + + $values['display']['page_3']['id'] = 'page_3'; + $values['display']['page_3']['display_title'] = 'Page 3'; + $values['display']['page_3']['display_plugin'] = 'page'; + $values['display']['page_3']['display_options']['path'] = ''; + $values['display']['embed']['id'] = 'embed'; $values['display']['embed']['display_title'] = 'Embedded'; $values['display']['embed']['display_plugin'] = 'embed'; @@ -115,6 +128,8 @@ public function testBuildRowEntityList() { ->will($this->returnValueMap(array( array('default', $values['display']['default'], $default_display), array('page', $values['display']['page_1'], $page_display), + array('page', $values['display']['page_2'], $page_display), + array('page', $values['display']['page_3'], $page_display), array('embed', $values['display']['embed'], $embed_display), ))); @@ -141,8 +156,16 @@ public function testBuildRowEntityList() { $row = $view_list_builder->buildRow($view); - $this->assertEquals(array('Embed admin label', 'Page admin label'), $row['data']['view_name']['data']['#displays'], 'Wrong displays got added to view list'); - $this->assertEquals($row['data']['path'], '/test_page', 'The path of the page display is not added.'); + $expected_displays = array( + 'Embed admin label', + 'Page admin label', + 'Page admin label', + 'Page admin label', + ); + $this->assertEquals($expected_displays, $row['data']['view_name']['data']['#displays']); + + $display_paths = $row['data']['path']['data']['#items']; + $this->assertEquals('/test_page, /<object>malformed_path</object>, /<script>alert("placeholder_page/%")</script>', implode(', ', $display_paths)); } }