Commit f71a3759 authored by alexpott's avatar alexpott

Issue #2546210 by penyaskito, mikeker, Gábor Hojtsy, dawehner: Views subtokens...

Issue #2546210 by penyaskito, mikeker, Gábor Hojtsy, dawehner: Views subtokens are broken since the introduction of inline templates for replacements
parent 4557d23d
<?php
/**
* @file
* Contains \Drupal\node\Tests\NodeFieldTokensTest.
*/
namespace Drupal\node\Tests\Views;
use Drupal\views\Views;
use Drupal\node\Tests\Views\NodeTestBase;
/**
* Tests replacement of Views tokens supplied by the Node module.
*
* @group node
* @see \Drupal\node\Tests\NodeTokenReplaceTest
*/
class NodeFieldTokensTest extends NodeTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_node_tokens');
/**
* Tests token replacement for Views tokens supplied by the Node module.
*/
public function testViewsTokenReplacement() {
// Create the Article content type with a standard body field.
/* @var $node_type \Drupal\node\NodeTypeInterface */
$node_type = entity_create('node_type', ['type' => '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 }}<br />
$this->assertRaw("Body: <p>$body</p>");
// Raw value: {{ body__value }}<br />
$this->assertRaw("Raw value: $body");
// Raw summary: {{ body__summary }}<br />
$this->assertRaw("Raw summary: $summary");
// Raw format: {{ body__format }}<br />
$this->assertRaw("Raw format: plain_text");
}
}
langcode: en
status: true
dependencies:
config:
- field.storage.node.body
module:
- node
- text
- user
id: test_node_tokens
label: test_node_tokens
module: views
description: 'Verifies tokens provided by the Node module are replaced correctly.'
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' previous'
next: 'next ›'
first: '« first'
last: 'last »'
quantity: 9
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
body:
id: body
table: node__body
field: body
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: true
text: "Body: {{ body }}<br />\nRaw value: {{ body__value }}<br />\nRaw summary: {{ body__summary }}<br />\nRaw format: {{ body__format }}"
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
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: ', '
field_api_classes: false
plugin_id: field
filters: { }
sorts:
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: ''
exposed: false
expose:
label: ''
granularity: second
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
cacheable: false
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test_node_tokens
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
cacheable: false
......@@ -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] : '';
}
}
......
......@@ -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());
}
}
......@@ -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 }}<br />\nThe taxonomy term ID for the term: {{ term_node_tid__tid }}<br />\nThe taxonomy term name for the term: {{ term_node_tid__name }}<br />\nThe machine name for the vocabulary the term belongs to: {{ term_node_tid__vocabulary_vid }}<br />\nThe name for the vocabulary the term belongs to: {{ term_node_tid__vocabulary }}<br />"
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: '<br />'
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
......@@ -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'];
}
}
......
......@@ -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 {
......
......@@ -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 . ' }}'] = '';
}
}
}
}
}
......
......@@ -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) { }
......
<?php
/**
* @file
* Contains \Drupal\views\Tests\Plugin\PluginBaseTest.
*/
namespace Drupal\views\Tests\Plugin;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\SafeString;
use Drupal\simpletest\KernelTestBase;
use Drupal\views\Plugin\views\PluginBase;
/**
* Tests the PluginBase class.
*
* @group views
*/
class PluginBaseTest extends KernelTestBase {
/**
* @var TestPluginBase
*/
var $testPluginBase;
public function setUp() {
parent::setUp();
$this->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);
}
}
......@@ -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();
}
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment