diff --git a/core/modules/node/src/Tests/Views/NodeFieldTokensTest.php b/core/modules/node/src/Tests/Views/NodeFieldTokensTest.php
new file mode 100644
index 0000000..06d1b2f
--- /dev/null
+++ b/core/modules/node/src/Tests/Views/NodeFieldTokensTest.php
@@ -0,0 +1,65 @@
+ 'article', 'name' => 'Article'));
+ $node_type->save();
+ node_add_body_field($node_type);
+
+ // Create a user and a node.
+ $account = $this->createUser();
+ $body = $this->randomMachineName(32);
+ $summary = $this->randomMachineName(16);
+
+ /** @var $node \Drupal\node\NodeInterface */
+ $node = entity_create('node', [
+ 'type' => 'article',
+ 'tnid' => 0,
+ 'uid' => $account->id(),
+ 'title' => 'Testing Views tokens',
+ 'body' => [['value' => $body, 'summary' => $summary, 'format' => 'plain_text']],
+ ]);
+ $node->save();
+
+ $this->drupalGet('test_node_tokens');
+
+ // Body: {{ body }}
+ $this->assertRaw("Body:
$body
");
+
+ // Raw value: {{ body__value }}
+ $this->assertRaw("Raw value: $body");
+
+ // Raw summary: {{ body__summary }}
+ $this->assertRaw("Raw summary: $summary");
+
+ // Raw format: {{ body__format }}
+ $this->assertRaw("Raw format: plain_text");
+ }
+
+}
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.taxonomy_all_terms_test.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_tokens.yml
similarity index 77%
copy from core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.taxonomy_all_terms_test.yml
copy to core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_tokens.yml
index 7e2673c..746a23c 100644
--- a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.taxonomy_all_terms_test.yml
+++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_tokens.yml
@@ -1,14 +1,16 @@
langcode: en
status: true
dependencies:
+ config:
+ - field.storage.node.body
module:
- node
- - taxonomy
+ - text
- user
-id: taxonomy_all_terms_test
-label: taxonomy_all_terms_test
+id: test_node_tokens
+label: test_node_tokens
module: views
-description: ''
+description: 'Verifies tokens provided by the Node module are replaced correctly.'
tag: ''
base_table: node_field_data
base_field: nid
@@ -81,18 +83,18 @@ display:
hide_empty: false
default_field_elements: true
fields:
- term_node_tid:
- id: term_node_tid
- table: node_field_data
- field: term_node_tid
+ body:
+ id: body
+ table: node__body
+ field: body
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
- alter_text: false
- text: ''
+ alter_text: true
+ text: "Body: {{ body }}
\nRaw value: {{ body__value }}
\nRaw summary: {{ body__summary }}
\nRaw format: {{ body__format }}"
make_link: false
path: ''
absolute: false
@@ -129,30 +131,37 @@ display:
hide_empty: false
empty_zero: false
hide_alter_empty: true
- type: separator
+ click_sort_column: value
+ type: text_default
+ settings: { }
+ group_column: value
+ group_columns: { }
+ group_rows: true
+ delta_limit: 0
+ delta_offset: 0
+ delta_reversed: false
+ delta_first_last: false
+ multi_type: separator
separator: ', '
- link_to_taxonomy: true
- limit: false
- vids:
- tags: '0'
- entity_type: node
- plugin_id: taxonomy_index_tid
+ field_api_classes: false
+ plugin_id: field
filters: { }
sorts:
- nid:
- id: nid
- table: node
- field: nid
+ created:
+ id: created
+ table: node_field_data
+ field: created
+ order: DESC
+ entity_type: node
+ entity_field: created
+ plugin_id: date
relationship: none
group_type: group
admin_label: ''
- order: ASC
exposed: false
expose:
label: ''
- entity_type: node
- entity_field: nid
- plugin_id: standard
+ granularity: second
header: { }
footer: { }
empty: { }
@@ -161,8 +170,9 @@ display:
display_extenders: { }
cache_metadata:
contexts:
+ - 'languages:language_content'
- 'languages:language_interface'
- - 'url.query_args.pagers:0'
+ - url.query_args
- 'user.node_grants:view'
- user.permissions
cacheable: false
@@ -173,11 +183,12 @@ display:
position: 1
display_options:
display_extenders: { }
- path: taxonomy_all_terms_test
+ path: test_node_tokens
cache_metadata:
contexts:
+ - 'languages:language_content'
- 'languages:language_interface'
- - 'url.query_args.pagers:0'
+ - url.query_args
- 'user.node_grants:view'
- user.permissions
cacheable: false
diff --git a/core/modules/taxonomy/src/Plugin/views/field/TaxonomyIndexTid.php b/core/modules/taxonomy/src/Plugin/views/field/TaxonomyIndexTid.php
index 4fab01c..3f40fc8 100644
--- a/core/modules/taxonomy/src/Plugin/views/field/TaxonomyIndexTid.php
+++ b/core/modules/taxonomy/src/Plugin/views/field/TaxonomyIndexTid.php
@@ -169,16 +169,15 @@ function render_item($count, $item) {
}
protected function documentSelfTokens(&$tokens) {
- $tokens['[' . $this->options['id'] . '-tid' . ']'] = $this->t('The taxonomy term ID for the term.');
- $tokens['[' . $this->options['id'] . '-name' . ']'] = $this->t('The taxonomy term name for the term.');
- $tokens['[' . $this->options['id'] . '-vocabulary-vid' . ']'] = $this->t('The machine name for the vocabulary the term belongs to.');
- $tokens['[' . $this->options['id'] . '-vocabulary' . ']'] = $this->t('The name for the vocabulary the term belongs to.');
+ $tokens['{{ ' . $this->options['id'] . '__tid' . ' }}'] = $this->t('The taxonomy term ID for the term.');
+ $tokens['{{ ' . $this->options['id'] . '__name' . ' }}'] = $this->t('The taxonomy term name for the term.');
+ $tokens['{{ ' . $this->options['id'] . '__vocabulary_vid' . ' }}'] = $this->t('The machine name for the vocabulary the term belongs to.');
+ $tokens['{{ ' . $this->options['id'] . '__vocabulary' . ' }}'] = $this->t('The name for the vocabulary the term belongs to.');
}
protected function addSelfTokens(&$tokens, $item) {
foreach (array('tid', 'name', 'vocabulary_vid', 'vocabulary') as $token) {
- // Replace _ with - for the vocabulary vid.
- $tokens['[' . $this->options['id'] . '-' . str_replace('_', '-', $token) . ']'] = isset($item[$token]) ? $item[$token] : '';
+ $tokens['{{ ' . $this->options['id'] . '__' . $token . ' }}'] = isset($item[$token]) ? $item[$token] : '';
}
}
diff --git a/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldAllTermsTest.php b/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldAllTermsTest.php
index f1ca0dd..8329d04 100644
--- a/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldAllTermsTest.php
+++ b/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldAllTermsTest.php
@@ -8,6 +8,7 @@
namespace Drupal\taxonomy\Tests\Views;
use Drupal\views\Views;
+use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests the "All terms" taxonomy term field handler.
@@ -23,7 +24,10 @@ class TaxonomyFieldAllTermsTest extends TaxonomyTestBase {
*/
public static $testViews = array('taxonomy_all_terms_test');
- function testViewsHandlerAllTermsField() {
+ /**
+ * Tests the "all terms" field handler.
+ */
+ public function testViewsHandlerAllTermsField() {
$view = Views::getView('taxonomy_all_terms_test');
$this->executeView($view);
$this->drupalGet('taxonomy_all_terms_test');
@@ -39,4 +43,28 @@ function testViewsHandlerAllTermsField() {
$this->assertEqual($actual[1]->__toString(), $this->term2->label());
}
+ /**
+ * Tests token replacement in the "all terms" field handler.
+ */
+ public function testViewsHandlerAllTermsWithTokens() {
+ $view = Views::getView('taxonomy_all_terms_test');
+ $this->drupalGet('taxonomy_all_terms_token_test');
+
+ // Term itself: {{ term_node_tid }}
+ $this->assertText('Term: ' . $this->term1->getName());
+
+ // The taxonomy term ID for the term: {{ term_node_tid__tid }}
+ $this->assertText('The taxonomy term ID for the term: ' . $this->term1->id());
+
+ // The taxonomy term name for the term: {{ term_node_tid__name }}
+ $this->assertText('The taxonomy term name for the term: ' . $this->term1->getName());
+
+ // The machine name for the vocabulary the term belongs to: {{ term_node_tid__vocabulary_vid }}
+ $this->assertText('The machine name for the vocabulary the term belongs to: ' . $this->term1->getVocabularyId());
+
+ // The name for the vocabulary the term belongs to: {{ term_node_tid__vocabulary }}
+ $vocabulary = Vocabulary::load($this->term1->bundle());
+ $this->assertText('The name for the vocabulary the term belongs to: ' . $vocabulary->label());
+ }
+
}
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.taxonomy_all_terms_test.yml b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.taxonomy_all_terms_test.yml
index 7e2673c..ce71f76 100644
--- a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.taxonomy_all_terms_test.yml
+++ b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.taxonomy_all_terms_test.yml
@@ -162,7 +162,7 @@ display:
cache_metadata:
contexts:
- 'languages:language_interface'
- - 'url.query_args.pagers:0'
+ - url.query_args
- 'user.node_grants:view'
- user.permissions
cacheable: false
@@ -177,7 +177,82 @@ display:
cache_metadata:
contexts:
- 'languages:language_interface'
- - 'url.query_args.pagers:0'
+ - url.query_args
+ - 'user.node_grants:view'
+ - user.permissions
+ cacheable: false
+ page_2:
+ display_plugin: page
+ id: page_2
+ display_title: 'Token tests'
+ position: 2
+ display_options:
+ display_extenders: { }
+ display_description: ''
+ fields:
+ term_node_tid:
+ id: term_node_tid
+ table: node_field_data
+ field: term_node_tid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: ''
+ exclude: false
+ alter:
+ alter_text: true
+ text: "Term: {{ term_node_tid }}
\nThe taxonomy term ID for the term: {{ term_node_tid__tid }}
\nThe taxonomy term name for the term: {{ term_node_tid__name }}
\nThe machine name for the vocabulary the term belongs to: {{ term_node_tid__vocabulary_vid }}
\nThe name for the vocabulary the term belongs to: {{ term_node_tid__vocabulary }}
"
+ 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
+ type: separator
+ separator: '
'
+ link_to_taxonomy: false
+ limit: false
+ vids:
+ tags: '0'
+ entity_type: node
+ plugin_id: taxonomy_index_tid
+ defaults:
+ fields: false
+ path: taxonomy_all_terms_token_test
+ cache_metadata:
+ contexts:
+ - 'languages:language_interface'
+ - url.query_args
- 'user.node_grants:view'
- user.permissions
cacheable: false
diff --git a/core/modules/user/src/Plugin/views/field/Roles.php b/core/modules/user/src/Plugin/views/field/Roles.php
index 403e9a4..00a1918 100644
--- a/core/modules/user/src/Plugin/views/field/Roles.php
+++ b/core/modules/user/src/Plugin/views/field/Roles.php
@@ -101,14 +101,14 @@ function render_item($count, $item) {
}
protected function documentSelfTokens(&$tokens) {
- $tokens['[' . $this->options['id'] . '-role' . ']'] = $this->t('The name of the role.');
- $tokens['[' . $this->options['id'] . '-rid' . ']'] = $this->t('The role machine-name of the role.');
+ $tokens['{{ ' . $this->options['id'] . '__role' . ' }}'] = $this->t('The name of the role.');
+ $tokens['{{ ' . $this->options['id'] . '__rid' . ' }}'] = $this->t('The role machine-name of the role.');
}
protected function addSelfTokens(&$tokens, $item) {
if (!empty($item['role'])) {
- $tokens['[' . $this->options['id'] . '-role' . ']'] = $item['role'];
- $tokens['[' . $this->options['id'] . '-rid' . ']'] = $item['rid'];
+ $tokens['{{ ' . $this->options['id'] . '__role' . ' }}'] = $item['role'];
+ $tokens['{{ ' . $this->options['id'] . '__rid' . ' }}'] = $item['rid'];
}
}
diff --git a/core/modules/views/src/Plugin/views/PluginBase.php b/core/modules/views/src/Plugin/views/PluginBase.php
index e56dcae..420437c 100644
--- a/core/modules/views/src/Plugin/views/PluginBase.php
+++ b/core/modules/views/src/Plugin/views/PluginBase.php
@@ -365,6 +365,12 @@ protected function viewsTokenReplace($text, $tokens) {
if (strpos($token, '{{') !== FALSE) {
// Twig wants a token replacement array stripped of curly-brackets.
$token = trim(str_replace(array('{', '}'), '', $token));
+
+ // We need to validate tokens are valid Twig variables. Twig uses the
+ // same variable naming rules as PHP.
+ // @see http://php.net/manual/en/language.variables.basics.php
+ assert('preg_match(\'/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/\', $token) === 1', 'Tokens need to be valid Twig variables.');
+
$twig_tokens[$token] = $replacement;
}
else {
diff --git a/core/modules/views/src/Plugin/views/field/Field.php b/core/modules/views/src/Plugin/views/field/Field.php
index 251d025..e4e3b0a 100644
--- a/core/modules/views/src/Plugin/views/field/Field.php
+++ b/core/modules/views/src/Plugin/views/field/Field.php
@@ -7,7 +7,7 @@
namespace Drupal\views\Plugin\views\field;
-use Drupal\Component\Utility\Xss as CoreXss;
+use Drupal\Component\Utility\Xss;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
@@ -670,7 +670,7 @@ public function renderItems($items) {
if (!empty($items)) {
$items = $this->prepareItemsByDelta($items);
if ($this->options['multi_type'] == 'separator' || !$this->options['group_rows']) {
- $separator = $this->options['multi_type'] == 'separator' ? CoreXss::filterAdmin($this->options['separator']) : '';
+ $separator = $this->options['multi_type'] == 'separator' ? Xss::filterAdmin($this->options['separator']) : '';
$build = [
'#type' => 'inline_template',
'#template' => '{{ items | safe_join(separator) }}',
@@ -903,7 +903,7 @@ function render_item($count, $item) {
protected function documentSelfTokens(&$tokens) {
$field = $this->getFieldDefinition();
foreach ($field->getColumns() as $id => $column) {
- $tokens['{{ ' . $this->options['id'] . '-' . $id . ' }}'] = $this->t('Raw @column', array('@column' => $id));
+ $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = $this->t('Raw @column', array('@column' => $id));
}
}
@@ -913,19 +913,29 @@ protected function addSelfTokens(&$tokens, $item) {
// Use \Drupal\Component\Utility\Xss::filterAdmin() because it's user data
// and we can't be sure it is safe. We know nothing about the data,
// though, so we can't really do much else.
-
if (isset($item['raw'])) {
- // If $item['raw'] is an array then we can use as is, if it's an object
- // we cast it to an array, if it's neither, we can't use it.
- $raw = is_array($item['raw']) ? $item['raw'] :
- (is_object($item['raw']) ? (array)$item['raw'] : NULL);
- }
- if (isset($raw) && isset($raw[$id]) && is_scalar($raw[$id])) {
- $tokens['{{ ' . $this->options['id'] . '-' . $id . ' }}'] = CoreXss::filterAdmin($raw[$id]);
- }
- else {
- // Make sure that empty values are replaced as well.
- $tokens['{{ ' . $this->options['id'] . '-' . $id . ' }}'] = '';
+ $raw = $item['raw'];
+
+ if (is_array($raw)) {
+ if (isset($raw[$id]) && is_scalar($raw[$id])) {
+ $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = Xss::filterAdmin($raw[$id]);
+ }
+ else {
+ // Make sure that empty values are replaced as well.
+ $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = '';
+ }
+ }
+
+ if (is_object($raw)) {
+ $property = $raw->get($id);
+ if (!empty($property)) {
+ $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = Xss::filterAdmin($property->getValue());
+ }
+ else {
+ // Make sure that empty values are replaced as well.
+ $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = '';
+ }
+ }
}
}
}
diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
index 996ef31..6107eb5 100644
--- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
+++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
@@ -1676,10 +1676,11 @@ protected function getTokenValuesRecursive(array $array, array $parent_keys = ar
* fields as a list. For example, the field that displays all terms
* on a node might have tokens for the tid and the term.
*
- * By convention, tokens should follow the format of {{ token-subtoken }}
+ * By convention, tokens should follow the format of {{ token
+ * subtoken }}
* where token is the field ID and subtoken is the field. If the
- * field ID is terms, then the tokens might be {{ terms-tid }} and
- * {{ terms-name }}.
+ * field ID is terms, then the tokens might be {{ terms__tid }} and
+ * {{ terms__name }}.
*/
protected function addSelfTokens(&$tokens, $item) { }
diff --git a/core/modules/views/src/Tests/Plugin/PluginBaseTest.php b/core/modules/views/src/Tests/Plugin/PluginBaseTest.php
new file mode 100644
index 0000000..f4b5c2d
--- /dev/null
+++ b/core/modules/views/src/Tests/Plugin/PluginBaseTest.php
@@ -0,0 +1,61 @@
+testPluginBase = new TestPluginBase();
+ }
+
+ /**
+ * Test that the token replacement in views works correctly.
+ */
+ public function testViewsTokenReplace() {
+ $text = '{{ langcode__value }} means {{ langcode }}';
+ $tokens = ['{{ langcode }}' => SafeString::create('English'), '{{ langcode__value }}' => 'en'];
+
+ $result = \Drupal::service('renderer')->executeInRenderContext(new RenderContext(), function () use ($text, $tokens) {
+ return $this->testPluginBase->viewsTokenReplace($text, $tokens);
+ });
+
+ $this->assertIdentical($result, 'en means English');
+ }
+
+}
+
+/**
+ * Helper class for using the PluginBase abstract class.
+ */
+class TestPluginBase extends PluginBase {
+
+ public function __construct() {
+ parent::__construct([], '', []);
+ }
+
+ public function viewsTokenReplace($text, $tokens) {
+ return parent::viewsTokenReplace($text, $tokens);
+ }
+
+}
diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/field/FieldTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/field/FieldTest.php
index 74d5d3d..0f8feef 100644
--- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/field/FieldTest.php
+++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/field/FieldTest.php
@@ -46,7 +46,7 @@ public function getTestValue() {
* Overrides Drupal\views\Plugin\views\field\FieldPluginBase::addSelfTokens().
*/
protected function addSelfTokens(&$tokens, $item) {
- $tokens['[test-token]'] = $this->getTestValue();
+ $tokens['[test__token]'] = $this->getTestValue();
}
/**