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(); } /**