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..9d7a63b
--- /dev/null
+++ b/core/modules/system/templates/inline-list.html.twig
@@ -0,0 +1,25 @@
+{#
+/**
+ * @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.
+ * - 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
+ */
+#}
+{%- for item in items -%}
+ {{ item.value }}{{ loop.last ? '' : separator }}
+{%- else -%}
+ {{- empty -}}
+{%- endfor -%}
+
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' => ''), 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('/');
+ $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(''),
+ $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'] = '';
+
+ $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));
}
}