diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc index 3294044..3946d3d 100644 --- a/core/modules/node/node.tokens.inc +++ b/core/modules/node/node.tokens.inc @@ -7,7 +7,6 @@ use Drupal\Component\Utility\Html; use Drupal\Core\Datetime\Entity\DateFormat; -use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Render\BubbleableMetadata; use Drupal\user\Entity\User; @@ -94,15 +93,16 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta $langcode = $options['langcode']; } else { - $langcode = LanguageInterface::LANGCODE_DEFAULT; + $langcode = NULL; } $sanitize = !empty($options['sanitize']); - $replacements = array(); + $replacements = []; if ($type == 'node' && !empty($data['node'])) { /** @var \Drupal\node\NodeInterface $node */ - $node = $data['node']; + $node = \Drupal::entityManager() + ->getTranslationFromContext($data['node'], $langcode, ['operation' => 'node_tokens']); foreach ($tokens as $name => $original) { switch ($name) { @@ -130,10 +130,8 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta case 'body': case 'summary': - $translation = \Drupal::entityManager()->getTranslationFromContext($node, $langcode, array('operation' => 'node_tokens')); - if ($translation->hasField('body') && ($items = $translation->get('body')) && !$items->isEmpty()) { + if ($node->hasField('body') && ($items = $node->get('body')) && !$items->isEmpty()) { $item = $items[0]; - $field_definition = \Drupal::entityManager()->getFieldDefinitions('node', $node->bundle())['body']; // If the summary was requested and is not empty, use it. if ($name == 'summary' && !empty($item->summary)) { $output = $sanitize ? $item->summary_processed : $item->summary; @@ -183,15 +181,14 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta break; case 'created': - $date_format = DateFormat::load('medium'); - $bubbleable_metadata->addCacheableDependency($date_format); - $replacements[$original] = format_date($node->getCreatedTime(), 'medium', '', NULL, $langcode); - break; - case 'changed': + /** @var \Drupal\Core\Datetime\Entity\DateFormat $date_format */ $date_format = DateFormat::load('medium'); + $date_raw = ($name === 'created' ? $node->getCreatedTime() : $node->getChangedTime()); + $date_formatted = \Drupal::service('date.formatter') + ->format($date_raw, $date_format->id(), '', NULL, $langcode); $bubbleable_metadata->addCacheableDependency($date_format); - $replacements[$original] = format_date($node->getChangedTime(), 'medium', '', NULL, $langcode); + $replacements[$original] = $sanitize ? Html::escape($date_formatted) : $date_formatted; break; } } diff --git a/core/modules/node/src/Tests/NodeTokenLanguageTest.php b/core/modules/node/src/Tests/NodeTokenLanguageTest.php new file mode 100644 index 0000000..b3a5e8b --- /dev/null +++ b/core/modules/node/src/Tests/NodeTokenLanguageTest.php @@ -0,0 +1,165 @@ +drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + + // Setup user. + $admin_user = $this->drupalCreateUser([ + 'administer languages', + 'administer content types', + 'access administration pages', + 'create page content', + 'edit own page content' + ]); + $this->drupalLogin($admin_user); + + // Add a new language. + ConfigurableLanguage::createFromLangcode('it')->save(); + + // Enable URL language detection and selection. + $edit = ['language_interface[enabled][language-url]' => '1']; + $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings')); + + // Set "Basic page" content type to use multilingual support. + $edit = ['language_configuration[language_alterable]' => TRUE]; + $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type')); + $this->assertRaw(t('The content type %type has been updated.', ['%type' => 'Basic page']), 'Basic page content type has been updated.'); + + // Make node body translatable. + $field_storage = FieldStorageConfig::loadByName('node', 'body'); + $field_storage->setTranslatable(TRUE); + $field_storage->save(); + } + + /** + * Test if node tokes are multilingual. + */ + public function testMultilingualNodeTokens() { + // Setup users. + foreach (['en', 'it'] as $langcode) { + $this->users[$langcode] = $this->drupalCreateUser([ + 'create page content', + 'edit own page content' + ]); + } + + // Create English "Basic page" node. + $langcode = 'en'; + $this->drupalLogin($this->users[$langcode]); + $fields = $this->getFieldsArray($langcode); + + $node = Node::create($fields); + $node->save(); + + $this->assertTokenReplacement($node); + + // Create italian translation with new field values. + $langcode = 'it'; + $this->drupalLogin($this->users[$langcode]); + $fields = $this->getFieldsArray($langcode); + $node->addTranslation($langcode, $fields); + $translation = $node->getTranslation($langcode); + + $this->assertTokenReplacement($translation); + } + + /** + * Populates an array with field values for a given language. + * + * @param string $langcode + * The language code. + * + * @return array + * The fields of node entity. + */ + protected function getFieldsArray($langcode) { + $time = REQUEST_TIME - rand(0, 1000); + return [ + 'title' => $langcode . $this->randomString(8), + 'body' => $langcode . $this->randomString(16), + 'type' => 'page', + 'language' => $langcode, + 'uid' => $this->users[$langcode]->id(), + 'created' => $time, + 'changed' => $time + 100, + ]; + } + + /** + * Asserts if tokens are correctly replaced. + * + * @param \Drupal\node\Entity\Node $node + * The node for which to test token replacement. + */ + protected function assertTokenReplacement(Node $node) { + // Perform unsanitized replacement for easy comparison. + $options = ['langcode' => $node->language()->getId(), 'sanitize' => FALSE]; + + $tokens = [ + '[node:title]' => [ + 'expected' => $node->title->value, + 'actual' => \Drupal::token()->replace('[node:title]', ['node' => $node], $options) + ], + '[node:body]' => [ + 'expected' => $node->body->value, + 'actual' => $token_replacement = \Drupal::token()->replace('[node:body]', ['node' => $node], $options) + ], + '[node:author]' => [ + 'expected' => $node->getOwner()->getUsername(), + 'actual' => \Drupal::token()->replace('[node:author]', ['node' => $node], $options) + ], + '[node:created]' => [ + 'expected' => \Drupal::service('date.formatter')->format($node->created->value, 'medium', '', NULL, $options['langcode']), + 'actual' => \Drupal::token()->replace('[node:created]', ['node' => $node], $options) + ], + '[node:changed]' => [ + 'expected' => \Drupal::service('date.formatter')->format($node->changed->value, 'medium', '', NULL, $options['langcode']), + 'actual' => \Drupal::token()->replace('[node:changed]', ['node' => $node], $options), + ], + ]; + + foreach ($tokens as $token => $replacements) { + $this->assertEqual($replacements['expected'], $replacements['actual'], $token . ' replaced successfully'); + } + } + +} diff --git a/core/modules/node/src/Tests/NodeTokenReplaceTest.php b/core/modules/node/src/Tests/NodeTokenReplaceTest.php index 3ec91d9..f8bd0bd 100644 --- a/core/modules/node/src/Tests/NodeTokenReplaceTest.php +++ b/core/modules/node/src/Tests/NodeTokenReplaceTest.php @@ -8,12 +8,14 @@ namespace Drupal\node\Tests; use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Render\BubbleableMetadata; +use Drupal\node\NodeTypeInterface; use Drupal\system\Tests\System\TokenReplaceUnitTestBase; +use Drupal\node\Entity\NodeType; /** - * Generates text using placeholders for dummy content to check node token - * replacement. + * Generates text using placeholders for dummy content to check node token replacement. * * @group node */ @@ -24,129 +26,209 @@ class NodeTokenReplaceTest extends TokenReplaceUnitTestBase { * * @var array */ - public static $modules = array('node', 'filter'); + public static $modules = ['filter', 'node']; + + /** + * The node type. + * + * @var \Drupal\node\NodeTypeInterface + */ + protected $nodeType; /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); - $this->installConfig(array('filter', 'node')); + $this->installConfig(static::$modules); - $node_type = entity_create('node_type', array('type' => 'article', 'name' => 'Article')); - $node_type->save(); - node_add_body_field($node_type); + /** @var NodeTypeInterface $node_type */ + $this->nodeType = NodeType::create([ + 'type' => 'article', + 'name' => 'My "<Article>"', + ]); + $this->nodeType->save(); + node_add_body_field($this->nodeType); } /** * Creates a node, then tests the tokens generated from it. */ - function testNodeTokenReplacement() { - $url_options = array( + public function testNodeTokenReplacement() { + $url_options = [ 'absolute' => TRUE, 'language' => $this->interfaceLanguage, - ); + ]; // Create a user and a node. $account = $this->createUser(); /* @var $node \Drupal\node\NodeInterface */ $node = entity_create('node', array( - 'type' => 'article', - 'tnid' => 0, + 'type' => $this->nodeType->id(), 'uid' => $account->id(), 'title' => 'Blinking Text', - 'body' => array(array('value' => $this->randomMachineName(32), 'summary' => $this->randomMachineName(16), 'format' => 'plain_text')), - )); - $node->save(); - - // Generate and test sanitized tokens. - $tests = array(); - $tests['[node:nid]'] = $node->id(); - $tests['[node:vid]'] = $node->getRevisionId(); - $tests['[node:type]'] = 'article'; - $tests['[node:type-name]'] = 'Article'; - $tests['[node:title]'] = Html::escape($node->getTitle()); - $tests['[node:body]'] = $node->body->processed; - $tests['[node:summary]'] = $node->body->summary_processed; - $tests['[node:langcode]'] = Html::escape($node->language()->getId()); - $tests['[node:url]'] = $node->url('canonical', $url_options); - $tests['[node:edit-url]'] = $node->url('edit-form', $url_options); - $tests['[node:author]'] = Html::escape($account->getUsername()); - $tests['[node:author:uid]'] = $node->getOwnerId(); - $tests['[node:author:name]'] = Html::escape($account->getUsername()); - $tests['[node:created:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($node->getCreatedTime(), array('langcode' => $this->interfaceLanguage->getId())); - $tests['[node:changed:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($node->getChangedTime(), array('langcode' => $this->interfaceLanguage->getId())); - - $base_bubbleable_metadata = BubbleableMetadata::createFromObject($node); - - $metadata_tests = []; - $metadata_tests['[node:nid]'] = $base_bubbleable_metadata; - $metadata_tests['[node:vid]'] = $base_bubbleable_metadata; - $metadata_tests['[node:type]'] = $base_bubbleable_metadata; - $metadata_tests['[node:type-name]'] = $base_bubbleable_metadata; - $metadata_tests['[node:title]'] = $base_bubbleable_metadata; - $metadata_tests['[node:body]'] = $base_bubbleable_metadata; - $metadata_tests['[node:summary]'] = $base_bubbleable_metadata; - $metadata_tests['[node:langcode]'] = $base_bubbleable_metadata; - $metadata_tests['[node:url]'] = $base_bubbleable_metadata; - $metadata_tests['[node:edit-url]'] = $base_bubbleable_metadata; - $bubbleable_metadata = clone $base_bubbleable_metadata; - $metadata_tests['[node:author]'] = $bubbleable_metadata->addCacheTags(['user:1']); - $metadata_tests['[node:author:uid]'] = $bubbleable_metadata; - $metadata_tests['[node:author:name]'] = $bubbleable_metadata; - $bubbleable_metadata = clone $base_bubbleable_metadata; - $metadata_tests['[node:created:since]'] = $bubbleable_metadata->setCacheMaxAge(0); - $metadata_tests['[node:changed:since]'] = $bubbleable_metadata; - - // Test to make sure that we generated something for each token. - $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); - - foreach ($tests as $input => $expected) { - $bubbleable_metadata = new BubbleableMetadata(); - $output = $this->tokenService->replace($input, array('node' => $node), array('langcode' => $this->interfaceLanguage->getId()), $bubbleable_metadata); - $this->assertEqual($output, $expected, format_string('Sanitized node token %token replaced.', array('%token' => $input))); - $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); - } - - // Generate and test unsanitized tokens. - $tests['[node:title]'] = $node->getTitle(); - $tests['[node:body]'] = $node->body->value; - $tests['[node:summary]'] = $node->body->summary; - $tests['[node:langcode]'] = $node->language()->getId(); - $tests['[node:author:name]'] = $account->getUsername(); - - foreach ($tests as $input => $expected) { - $output = $this->tokenService->replace($input, array('node' => $node), array('langcode' => $this->interfaceLanguage->getId(), 'sanitize' => FALSE)); - $this->assertEqual($output, $expected, format_string('Unsanitized node token %token replaced.', array('%token' => $input))); - } - - // Repeat for a node without a summary. - $node = entity_create('node', array( - 'type' => 'article', + 'body' => [ + [ + 'value' => 'Blinking Body', + 'summary' => 'Blinking Summary', + 'format' => 'plain_text', + ], + ], + )); + $node->save(); + + // Generate and test sanitized tokens. + $tests = array(); + $tests['[node:nid]'] = $node->id(); + $tests['[node:vid]'] = $node->getRevisionId(); + $tests['[node:type]'] = $this->nodeType->id(); + $tests['[node:type-name]'] = Html::escape($this->nodeType->label()); + $tests['[node:title]'] = Html::escape($node->getTitle()); + $tests['[node:body]'] = $node->body->processed; + $tests['[node:summary]'] = $node->body->summary_processed; + $tests['[node:langcode]'] = Html::escape($node->language()->getId()); + $tests['[node:url]'] = $node->url('canonical', $url_options); + $tests['[node:edit-url]'] = $node->url('edit-form', $url_options); + $tests['[node:author]'] = Html::escape($account->getUsername()); + $tests['[node:author:uid]'] = $node->getOwnerId(); + $tests['[node:author:name]'] = Html::escape($account->getUsername()); + $tests['[node:created:since]'] = \Drupal::service('date.formatter') + ->formatTimeDiffSince($node->getCreatedTime(), array('langcode' => $this->interfaceLanguage->getId())); + $tests['[node:changed:since]'] = \Drupal::service('date.formatter') + ->formatTimeDiffSince($node->getChangedTime(), array('langcode' => $this->interfaceLanguage->getId())); + + $base_bubbleable_metadata = BubbleableMetadata::createFromObject($node); + $author_bubbleable_metadata = clone $base_bubbleable_metadata; + $date_bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests = []; + $metadata_tests['[node:nid]'] = $base_bubbleable_metadata; + $metadata_tests['[node:vid]'] = $base_bubbleable_metadata; + $metadata_tests['[node:type]'] = $base_bubbleable_metadata; + $metadata_tests['[node:type-name]'] = $base_bubbleable_metadata; + $metadata_tests['[node:title]'] = $base_bubbleable_metadata; + $metadata_tests['[node:body]'] = $base_bubbleable_metadata; + $metadata_tests['[node:summary]'] = $base_bubbleable_metadata; + $metadata_tests['[node:langcode]'] = $base_bubbleable_metadata; + $metadata_tests['[node:url]'] = $base_bubbleable_metadata; + $metadata_tests['[node:edit-url]'] = $base_bubbleable_metadata; + $metadata_tests['[node:author]'] = $author_bubbleable_metadata->addCacheTags(['user:1']); + $metadata_tests['[node:author:uid]'] = $author_bubbleable_metadata; + $metadata_tests['[node:author:name]'] = $author_bubbleable_metadata; + $metadata_tests['[node:created:since]'] = $date_bubbleable_metadata->setCacheMaxAge(0); + $metadata_tests['[node:changed:since]'] = $date_bubbleable_metadata; + + // Test to make sure that we generated something for each token. + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); + + $data = ['node' => $node]; + $options = [ + 'langcode' => $this->interfaceLanguage->getId(), + 'sanitize' => TRUE, + ]; + $msg = 'Sanitized node token %token replaced with %output. Expected is %expected'; + $this->assertTokenReplacementAndCheckMetadata($tests, $data, $options, $msg, $metadata_tests); + + // Generate and test unsanitized tokens. + $tests['[node:type-name]'] = $this->nodeType->label(); + $tests['[node:title]'] = $node->getTitle(); + $tests['[node:body]'] = $node->body->value; + $tests['[node:summary]'] = $node->body->summary; + $tests['[node:langcode]'] = $node->language()->getId(); + $tests['[node:author]'] = $account->getUsername(); + $tests['[node:author:name]'] = $account->getUsername(); + $options['sanitize'] = FALSE; + $msg = 'Unsanitized node token %token replaced with %output. Expected is %expected'; + $this->assertTokenReplacement($tests, $data, $options, $msg); + + // Repeat for a node without a summary. + $node = entity_create('node', array( + 'type' => $this->nodeType->id(), 'uid' => $account->id(), 'title' => 'Blinking Text', - 'body' => array(array('value' => $this->randomMachineName(32), 'format' => 'plain_text')), - )); - $node->save(); - - // Generate and test sanitized token - use full body as expected value. - $tests = array(); - $tests['[node:summary]'] = $node->body->processed; - - // Test to make sure that we generated something for each token. - $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated for node without a summary.'); + 'body' => [ + [ + 'value' => 'Blinking Body without Summary', + 'format' => 'plain_text', + ], + ], + )); + $node->save(); + + // Generate and test sanitized token - use full body as expected value. + $tests = [ + '[node:summary]' => $node->body->processed, + ]; + + // Test to make sure that we generated something for each token. + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated for node without a summary.'); + + $data = ['node' => $node]; + $options['sanitize'] = TRUE; + $msg = 'Sanitized node token %token replaced with %output for node without a summary. Expected is %expected'; + $this->assertTokenReplacement($tests, $data, $options, $msg); + + // Generate and test unsanitized tokens. + $tests['[node:summary]'] = $node->body->value; + + $options['sanitize'] = FALSE; + $msg = 'Unsanitized node token %token replaced with %output for node without a summary. Expected is %expected'; + $this->assertTokenReplacement($tests, $data, $options, $msg); + } - foreach ($tests as $input => $expected) { - $output = $this->tokenService->replace($input, array('node' => $node), array('language' => $this->interfaceLanguage)); - $this->assertEqual($output, $expected, format_string('Sanitized node token %token replaced for node without a summary.', array('%token' => $input))); + /** + * Asserts if tokens are correctly replaced and verifies that the correct metadata has been set. + * + * @param array $tests + * Expected results keyed by their respective tokens. + * @param array $data + * The data to perform the replacement on. @see Token::replace(). + * @param array $options + * Additional replacement options. @see Token::replace(). + * @param $message + * The message to display with the assertion. Can contain replacement tokens: + * - %token for the token name + * - %output for the value returned by the token replacement service + * - %expected for the expected result + * @param array $metadata_tests + * The metadata to verify. Keyed by the relevant token. + */ + protected function assertTokenReplacementAndCheckMetadata(array $tests, array $data, array $options, $message, array $metadata_tests) { + foreach ($tests as $token => $expected) { + $bubbleable_metadata = new BubbleableMetadata(); + $output = $this->tokenService->replace($token, $data, $options, $bubbleable_metadata); + $this->assertEqual($output, $expected, SafeMarkup::format($message, [ + '%token' => $token, + '%output' => $output, + '%expected' => $expected, + ])); + + $this->assertEqual($bubbleable_metadata, $metadata_tests[$token]); } + } - // Generate and test unsanitized tokens. - $tests['[node:summary]'] = $node->body->value; - - foreach ($tests as $input => $expected) { - $output = $this->tokenService->replace($input, array('node' => $node), array('language' => $this->interfaceLanguage, 'sanitize' => FALSE)); - $this->assertEqual($output, $expected, format_string('Unsanitized node token %token replaced for node without a summary.', array('%token' => $input))); + /** + * Asserts if tokens are correctly replaced. + * + * @param array $tests + * Expected results keyed by their respective tokens. + * @param array $data + * The data to perform the replacement on. @see Token::replace(). + * @param array $options + * Aditional replacement options. @see Token::replace(). + * @param $message + * The message to display with the assertion. Can contain replacement tokens: + * - %token for the token name + * - %output for the value returned by the token replacement service + * - %expected for the expected result + */ + protected function assertTokenReplacement(array $tests, array $data, array $options, $message) { + foreach ($tests as $token => $expected) { + $output = $this->tokenService->replace($token, $data, $options); + $this->assertEqual($output, $expected, SafeMarkup::format($message, [ + '%token' => $token, + '%output' => $output, + '%expected' => $expected, + ])); } } diff --git a/core/modules/taxonomy/src/Tests/TaxonomyTestTrait.php b/core/modules/taxonomy/src/Tests/TaxonomyTestTrait.php index 34971db..c7c4f93 100644 --- a/core/modules/taxonomy/src/Tests/TaxonomyTestTrait.php +++ b/core/modules/taxonomy/src/Tests/TaxonomyTestTrait.php @@ -18,10 +18,16 @@ /** * Returns a new vocabulary with random properties. + * + * @param array $values + * (optional) An array of values to set, keyed by property name. + * + * @return \Drupal\taxonomy\VocabularyInterface + * The new taxonomy vocabulary object. */ - function createVocabulary() { + function createVocabulary(array $values = array()) { // Create a vocabulary. - $vocabulary = entity_create('taxonomy_vocabulary', array( + $vocabulary = entity_create('taxonomy_vocabulary', $values + array( 'name' => $this->randomMachineName(), 'description' => $this->randomMachineName(), 'vid' => Unicode::strtolower($this->randomMachineName()), diff --git a/core/modules/taxonomy/src/Tests/TaxonomyTokenLanguageTest.php b/core/modules/taxonomy/src/Tests/TaxonomyTokenLanguageTest.php new file mode 100644 index 0000000..cc23d4d --- /dev/null +++ b/core/modules/taxonomy/src/Tests/TaxonomyTokenLanguageTest.php @@ -0,0 +1,121 @@ +drupalLogin($this->drupalCreateUser(['administer taxonomy'])); + + // Create a vocabulary to which the terms will be assigned. + $this->vocabulary = $this->createVocabulary(); + + // Add a new language. + ConfigurableLanguage::createFromLangcode('it')->save(); + } + + /** + * Test if taxonomy token are multilingual. + */ + public function testMultilingualTaxonomyTokens() { + // Configure the vocabulary to not hide the language selector. + $edit = [ + 'default_language[language_alterable]' => TRUE, + ]; + $this->drupalPostForm('admin/structure/taxonomy/manage/' . $this->vocabulary->id(), $edit, t('Save')); + + // Create Term. + $langcode = 'en'; + $fields = $this->getFieldsArray($langcode); + $term = $this->createTerm($this->vocabulary, $fields); + + $this->verifyTokensCorrectlyReplaced($term); + + $langcode = 'it'; + $fields = $this->getFieldsArray($langcode); + $term = $term->addTranslation($langcode, $fields); + + $this->verifyTokensCorrectlyReplaced($term); + } + + /** + * Populates an array with field values for a given language. + * + * @param string $langcode + * The language code. + * + * @return array + * The fields of a term. + */ + protected function getFieldsArray($langcode) { + return [ + 'name' => $langcode . $this->randomString(8), + 'description' => $langcode . $this->randomString(16), + 'langcode' => $langcode, + ]; + } + + /** + * Verifies if tokens were correctly replaced. + * + * @param \Drupal\taxonomy\Entity\Term $term + * The term that needs to be checked. + */ + protected function verifyTokensCorrectlyReplaced(Term $term) { + // Perform unsanitized replacement for easy comparison. + $options = ['langcode' => $term->language()->getId(), 'sanitize' => FALSE]; + + $tokens = [ + '[term:name]' => [ + 'expected' => $term->name->value, + 'actual' => \Drupal::token()->replace('[term:name]', ['term' => $term], $options) + ], + '[term:description]' => [ + 'expected' => $term->description->value, + 'actual' => $token_replacement = \Drupal::token()->replace('[term:description]', ['term' => $term], $options) + ], + '[term:url]' => [ + 'expected' => $term->url('canonical', array('absolute' => TRUE)), + 'actual' => \Drupal::token()->replace('[term:url]', ['term' => $term], $options) + ] + ]; + + foreach ($tokens as $token => $replacements) { + $this->assertEqual($replacements['expected'], $replacements['actual'], $token . ' replaced successfully'); + } + } + +} diff --git a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php index 4fcb0f1..3432009 100644 --- a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php +++ b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php @@ -8,6 +8,7 @@ namespace Drupal\taxonomy\Tests; use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Xss; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Render\BubbleableMetadata; @@ -23,7 +24,7 @@ class TokenReplaceTest extends TaxonomyTestBase { /** * The vocabulary used for creating terms. * - * @var \Drupal\taxonomy\VocabularyInterface + * @var \Drupal\taxonomy\Entity\Vocabulary */ protected $vocabulary; @@ -34,10 +35,22 @@ class TokenReplaceTest extends TaxonomyTestBase { */ protected $fieldName; + /** + * Token service. + * + * @var \Drupal\Core\Utility\Token + */ + protected $tokenService; + protected function setUp() { parent::setUp(); + + $this->tokenService = \Drupal::token(); + $this->drupalLogin($this->drupalCreateUser(['administer taxonomy', 'bypass node access'])); - $this->vocabulary = $this->createVocabulary(); + $this->vocabulary = $this->createVocabulary([ + 'name' => 'V1 "<name>"', + ]); $this->fieldName = 'taxonomy_' . $this->vocabulary->id(); $handler_settings = array( @@ -64,23 +77,28 @@ protected function setUp() { * Creates some terms and a node, then tests the tokens generated from them. */ function testTaxonomyTokenReplacement() { - $token_service = \Drupal::token(); $language_interface = \Drupal::languageManager()->getCurrentLanguage(); - // Create two taxonomy terms. - $term1 = $this->createTerm($this->vocabulary); - $term2 = $this->createTerm($this->vocabulary); + // Create two taxonomy terms with unsafe names. + $term1 = $this->createTerm($this->vocabulary, [ + 'name' => 'T1 ', + ]); + $term2 = $this->createTerm($this->vocabulary, [ + 'name' => 'T2 "<name>"', + ]); // Edit $term2, setting $term1 as parent. - $edit = array(); - $edit['name[0][value]'] = 'Blinking Text'; - $edit['parent[]'] = array($term1->id()); + $edit = [ + 'name[0][value]' => 'Blinking Text', + 'parent[]' => [$term1->id()], + ]; $this->drupalPostForm('taxonomy/term/' . $term2->id() . '/edit', $edit, t('Save')); // Create node with term2. - $edit = array(); - $node = $this->drupalCreateNode(array('type' => 'article')); - $edit[$this->fieldName . '[]'] = $term2->id(); + $node = $this->drupalCreateNode(['type' => 'article']); + $edit = [ + $this->fieldName . '[]' => $term2->id(), + ]; $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); // Generate and test sanitized tokens for term1. @@ -90,29 +108,34 @@ function testTaxonomyTokenReplacement() { $tests['[term:description]'] = $term1->description->processed; $tests['[term:url]'] = $term1->url('canonical', array('absolute' => TRUE)); $tests['[term:node-count]'] = 0; + $tests['[term:parent]'] = '[term:parent]'; $tests['[term:parent:name]'] = '[term:parent:name]'; - $tests['[term:vocabulary:name]'] = Html::escape($this->vocabulary->label()); + $tests['[term:parent:url]'] = '[term:parent:url]'; $tests['[term:vocabulary]'] = Html::escape($this->vocabulary->label()); + $tests['[term:vocabulary:name]'] = Html::escape($this->vocabulary->label()); $base_bubbleable_metadata = BubbleableMetadata::createFromObject($term1); - $metadata_tests = array(); + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests = []; $metadata_tests['[term:tid]'] = $base_bubbleable_metadata; $metadata_tests['[term:name]'] = $base_bubbleable_metadata; $metadata_tests['[term:description]'] = $base_bubbleable_metadata; $metadata_tests['[term:url]'] = $base_bubbleable_metadata; $metadata_tests['[term:node-count]'] = $base_bubbleable_metadata; + $metadata_tests['[term:parent]'] = $base_bubbleable_metadata; $metadata_tests['[term:parent:name]'] = $base_bubbleable_metadata; - $bubbleable_metadata = clone $base_bubbleable_metadata; - $metadata_tests['[term:vocabulary:name]'] = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()); + $metadata_tests['[term:parent:url]'] = $base_bubbleable_metadata; $metadata_tests['[term:vocabulary]'] = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()); + $metadata_tests['[term:vocabulary:name]'] = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()); - foreach ($tests as $input => $expected) { - $bubbleable_metadata = new BubbleableMetadata(); - $output = $token_service->replace($input, array('term' => $term1), array('langcode' => $language_interface->getId()), $bubbleable_metadata); - $this->assertEqual($output, $expected, format_string('Sanitized taxonomy term token %token replaced.', array('%token' => $input))); - $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); - } + $data = ['term' => $term1]; + $options = [ + 'langcode' => $language_interface->getId(), + 'sanitize' => TRUE, + ]; + $msg = 'Sanitized taxonomy term 1 token %token replaced with %output. Expected is %expected'; + $this->assertTokenReplacementAndCheckMetadata($tests, $data, $options, $msg, $metadata_tests); // Generate and test sanitized tokens for term2. $tests = array(); @@ -121,29 +144,32 @@ function testTaxonomyTokenReplacement() { $tests['[term:description]'] = $term2->description->processed; $tests['[term:url]'] = $term2->url('canonical', array('absolute' => TRUE)); $tests['[term:node-count]'] = 1; + $tests['[term:parent]'] = Html::escape($term1->getName()); $tests['[term:parent:name]'] = Html::escape($term1->getName()); $tests['[term:parent:url]'] = $term1->url('canonical', array('absolute' => TRUE)); $tests['[term:parent:parent:name]'] = '[term:parent:parent:name]'; + $tests['[term:vocabulary]'] = Html::escape($this->vocabulary->label()); $tests['[term:vocabulary:name]'] = Html::escape($this->vocabulary->label()); // Test to make sure that we generated something for each token. $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); - foreach ($tests as $input => $expected) { - $output = $token_service->replace($input, array('term' => $term2), array('langcode' => $language_interface->getId())); - $this->assertEqual($output, $expected, format_string('Sanitized taxonomy term token %token replaced.', array('%token' => $input))); - } + $data = ['term' => $term2]; + $options['sanitize'] = TRUE; + $msg = 'Sanitized taxonomy term 2 token %token replaced with %output. Expected is %expected'; + $this->assertTokenReplacement($tests, $data, $options, $msg); // Generate and test unsanitized tokens. $tests['[term:name]'] = $term2->getName(); $tests['[term:description]'] = $term2->getDescription(); + $tests['[term:parent]'] = $term1->getName(); $tests['[term:parent:name]'] = $term1->getName(); + $tests['[term:vocabulary]'] = $this->vocabulary->label(); $tests['[term:vocabulary:name]'] = $this->vocabulary->label(); - foreach ($tests as $input => $expected) { - $output = $token_service->replace($input, array('term' => $term2), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE)); - $this->assertEqual($output, $expected, format_string('Unsanitized taxonomy term token %token replaced.', array('%token' => $input))); - } + $options['sanitize'] = FALSE; + $msg = 'Unsanitized taxonomy term 2 token %token replaced with %output. Expected is %expected'; + $this->assertTokenReplacement($tests, $data, $options, $msg); // Generate and test sanitized tokens. $tests = array(); @@ -156,18 +182,75 @@ function testTaxonomyTokenReplacement() { // Test to make sure that we generated something for each token. $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); - foreach ($tests as $input => $expected) { - $output = $token_service->replace($input, array('vocabulary' => $this->vocabulary), array('langcode' => $language_interface->getId())); - $this->assertEqual($output, $expected, format_string('Sanitized taxonomy vocabulary token %token replaced.', array('%token' => $input))); - } + $data = ['vocabulary' => $this->vocabulary]; + $options['sanitize'] = TRUE; + $msg = 'Sanitized taxonomy vocabulary token %token replaced with %output. Expected is %expected'; + $this->assertTokenReplacement($tests, $data, $options, $msg); // Generate and test unsanitized tokens. $tests['[vocabulary:name]'] = $this->vocabulary->label(); $tests['[vocabulary:description]'] = $this->vocabulary->getDescription(); - foreach ($tests as $input => $expected) { - $output = $token_service->replace($input, array('vocabulary' => $this->vocabulary), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE)); - $this->assertEqual($output, $expected, format_string('Unsanitized taxonomy vocabulary token %token replaced.', array('%token' => $input))); + $options['sanitize'] = FALSE; + $msg = 'Unsanitized taxonomy vocabulary token %token replaced with %output. Expected is %expected.'; + $this->assertTokenReplacement($tests, $data, $options, $msg); + } + + /** + * Asserts if tokens are correctly replaced and verifies that the correct + * metadata has been set. + * + * @param array $tests + * Expected results keyed by their respective tokens. + * @param array $data + * The data to perform the replacement on. @see Token::replace(). + * @param array $options + * Aditional replacement options. @see Token::replace(). + * @param $message + * The message to display with the assertion. Can contain replacement tokens: + * - %token for the token name + * - %output for the value returned by the token replacement service + * - %expected for the expected result + * @param array $metadata_tests + * The metadata to verify. Keyed by the relevant token. + */ + protected function assertTokenReplacementAndCheckMetadata(array $tests, array $data, array $options, $message, array $metadata_tests) { + foreach ($tests as $token => $expected) { + $bubbleable_metadata = new BubbleableMetadata(); + $output = $this->tokenService->replace($token, $data, $options, $bubbleable_metadata); + $this->assertEqual($output, $expected, SafeMarkup::format($message, [ + '%token' => $token, + '%output' => $output, + '%expected' => $expected, + ])); + + $this->assertEqual($bubbleable_metadata, $metadata_tests[$token]); + } + } + + /** + * Asserts if tokens are correctly replaced. + * + * @param array $tests + * Expected results keyed by their respective tokens. + * @param array $data + * The data to perform the replacement on. @see Token::replace(). + * @param array $options + * Aditional replacement options. @see Token::replace(). + * @param $message + * The message to display with the assertion. Can contain replacement tokens: + * - %token for the token name + * - %output for the value returned by the token replacement service + * - %expected for the expected result + */ + protected function assertTokenReplacement(array $tests, array $data, array $options, $message) { + foreach ($tests as $token => $expected) { + $output = $this->tokenService->replace($token, $data, $options); + $this->assertEqual($output, $expected, SafeMarkup::format($message, [ + '%token' => $token, + '%output' => $output, + '%expected' => $expected, + ])); } } } diff --git a/core/modules/taxonomy/taxonomy.tokens.inc b/core/modules/taxonomy/taxonomy.tokens.inc index daf690b..73d0a5c 100644 --- a/core/modules/taxonomy/taxonomy.tokens.inc +++ b/core/modules/taxonomy/taxonomy.tokens.inc @@ -94,13 +94,32 @@ function taxonomy_token_info() { * Implements hook_tokens(). */ function taxonomy_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { + // Return if the $type is not supported or the required data is missing. + if (($type != 'term' && $type != 'vocabulary') + || ($type == 'term' && empty($data['term'])) + || ($type == 'vocabulary' && empty($data['vocabulary'])) + ) { + return []; + } + + $replacements = []; $token_service = \Drupal::token(); - $replacements = array(); + $url_options = ['absolute' => TRUE]; + if (isset($options['langcode'])) { + $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']); + $langcode = $options['langcode']; + } + else { + $langcode = NULL; + } $sanitize = !empty($options['sanitize']); + + /** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */ $taxonomy_storage = \Drupal::entityManager()->getStorage('taxonomy_term'); - if ($type == 'term' && !empty($data['term'])) { - $term = $data['term']; + if ($type == 'term') { + /** @var \Drupal\taxonomy\TermInterface $term */ + $term = \Drupal::entityManager()->getTranslationFromContext($data['term'], $langcode, ['operation' => 'term_tokens']); foreach ($tokens as $name => $original) { switch ($name) { @@ -117,7 +136,7 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable break; case 'url': - $replacements[$original] = $term->url('canonical', array('absolute' => TRUE)); + $replacements[$original] = $term->url('canonical', $url_options); break; case 'node-count': @@ -129,16 +148,17 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable break; case 'vocabulary': + /** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */ $vocabulary = Vocabulary::load($term->bundle()); $bubbleable_metadata->addCacheableDependency($vocabulary); - $replacements[$original] = Html::escape($vocabulary->label()); + $replacements[$original] = $sanitize ? Html::escape($vocabulary->label()) : $vocabulary->label(); break; case 'parent': if ($parents = $taxonomy_storage->loadParents($term->id())) { - $parent = array_pop($parents); - $bubbleable_metadata->addCacheableDependency($parent); - $replacements[$original] = Html::escape($parent->getName()); + $term_first_parent = \Drupal::entityManager()->getTranslationFromContext(array_pop($parents), $langcode, ['operation' => 'term_tokens']); + $bubbleable_metadata->addCacheableDependency($term_first_parent); + $replacements[$original] = $sanitize ? Html::escape($term_first_parent->getName()) : $term_first_parent->getName(); } break; } @@ -149,14 +169,16 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable $replacements += $token_service->generate('vocabulary', $vocabulary_tokens, array('vocabulary' => $vocabulary), $options, $bubbleable_metadata); } - if (($vocabulary_tokens = $token_service->findWithPrefix($tokens, 'parent')) && $parents = $taxonomy_storage->loadParents($term->id())) { - $parent = array_pop($parents); - $replacements += $token_service->generate('term', $vocabulary_tokens, array('term' => $parent), $options, $bubbleable_metadata); + if (($term_parent_tokens = $token_service->findWithPrefix($tokens, 'parent')) + && $term_parents = $taxonomy_storage->loadParents($term->id()) + ) { + $term_first_parent = array_pop($term_parents); + $replacements += $token_service->generate('term', $term_parent_tokens, array('term' => $term_first_parent), $options, $bubbleable_metadata); } } - - elseif ($type == 'vocabulary' && !empty($data['vocabulary'])) { - $vocabulary = $data['vocabulary']; + elseif ($type == 'vocabulary') { + /** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */ + $vocabulary = \Drupal::entityManager()->getTranslationFromContext($data['vocabulary'], $langcode, ['operation' => 'vocabulary_tokens']); foreach ($tokens as $name => $original) { switch ($name) {