diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc index 30bd1c6b95..aa89277b99 100644 --- a/core/modules/node/node.tokens.inc +++ b/core/modules/node/node.tokens.inc @@ -6,7 +6,6 @@ */ use Drupal\Core\Datetime\Entity\DateFormat; -use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Render\BubbleableMetadata; use Drupal\user\Entity\User; @@ -93,13 +92,13 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta $langcode = $options['langcode']; } else { - $langcode = LanguageInterface::LANGCODE_DEFAULT; + $langcode = NULL; } $replacements = []; if ($type == 'node' && !empty($data['node'])) { /** @var \Drupal\node\NodeInterface $node */ - $node = $data['node']; + $node = \Drupal::service('entity.repository')->getTranslationFromContext($data['node'], $langcode, ['operation' => 'node_tokens']); foreach ($tokens as $name => $original) { switch ($name) { @@ -127,8 +126,7 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta case 'body': case 'summary': - $translation = \Drupal::service('entity.repository')->getTranslationFromContext($node, $langcode, ['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]; // If the summary was requested and is not empty, use it. if ($name == 'summary' && !empty($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] = \Drupal::service('date.formatter')->format($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] = \Drupal::service('date.formatter')->format($node->getChangedTime(), 'medium', '', NULL, $langcode); + $replacements[$original] = $date_formatted; break; } } diff --git a/core/modules/node/tests/src/Kernel/NodeTokenReplaceTest.php b/core/modules/node/tests/src/Kernel/NodeTokenReplaceTest.php index 12f72f5347..b3ea52328a 100644 --- a/core/modules/node/tests/src/Kernel/NodeTokenReplaceTest.php +++ b/core/modules/node/tests/src/Kernel/NodeTokenReplaceTest.php @@ -10,6 +10,8 @@ use Drupal\Tests\system\Kernel\Token\TokenReplaceKernelTestBase; /** + * Generates text using placeholders. + * * Generates text using placeholders for dummy content to check node token * replacement. * diff --git a/core/modules/system/src/Tests/Utility/AssertTokenReplacementTrait.php b/core/modules/system/src/Tests/Utility/AssertTokenReplacementTrait.php new file mode 100644 index 0000000000..4c9f4fcf28 --- /dev/null +++ b/core/modules/system/src/Tests/Utility/AssertTokenReplacementTrait.php @@ -0,0 +1,84 @@ + $expected) { + $bubbleable_metadata = new BubbleableMetadata(); + $output = \Drupal::token()->replace($token, $data, $options, $bubbleable_metadata); + $this->assertEqual($output, new HtmlEscapedText($expected), new FormattableMarkup($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 + * 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 + */ + protected function assertTokenReplacement(array $tests, array $data, array $options, $message) { + foreach ($tests as $token => $expected) { + $output = \Drupal::token()->replace($token, $data, $options); + $expected = ($expected instanceof MarkupInterface) ? $expected : new HtmlEscapedText($expected); + $this->assertEqual($output, $expected, new FormattableMarkup($message, [ + '%token' => $token, + '%output' => $output, + '%expected' => $expected, + ])); + } + } + +} diff --git a/core/modules/taxonomy/taxonomy.tokens.inc b/core/modules/taxonomy/taxonomy.tokens.inc index 39d7da5e14..87d6163992 100644 --- a/core/modules/taxonomy/taxonomy.tokens.inc +++ b/core/modules/taxonomy/taxonomy.tokens.inc @@ -95,9 +95,14 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable $token_service = \Drupal::token(); $replacements = []; + + $langcode = isset($options['langcode']) ? $options['langcode'] : NULL; + + /** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */ $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term'); if ($type == 'term' && !empty($data['term'])) { - $term = $data['term']; + /** @var \Drupal\taxonomy\TermInterface $term */ + $term = \Drupal::service('entity.repository')->getTranslationFromContext($data['term'], $langcode, ['operation' => 'term_tokens']); foreach ($tokens as $name => $original) { switch ($name) { @@ -110,9 +115,7 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable break; case 'description': - // "processed" returns a \Drupal\Component\Render\MarkupInterface via - // check_markup(). - $replacements[$original] = $term->description->processed; + $replacements[$original] = $term->getDescription(); break; case 'url': @@ -128,6 +131,7 @@ 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] = $vocabulary->label(); @@ -135,7 +139,7 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable case 'parent': if ($parents = $taxonomy_storage->loadParents($term->id())) { - $parent = array_pop($parents); + $parent = \Drupal::service('entity.repository')->getTranslationFromContext(array_pop($parents), $langcode, ['operation' => 'term_tokens']); $bubbleable_metadata->addCacheableDependency($parent); $replacements[$original] = $parent->getName(); } @@ -155,6 +159,11 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable } elseif ($type == 'vocabulary' && !empty($data['vocabulary'])) { + /** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */ + $language_manager = \Drupal::languageManager(); + $language = $language_manager->getLanguage($langcode); + $original_language = $language_manager->getConfigOverrideLanguage(); + $language_manager->setConfigOverrideLanguage($language); $vocabulary = $data['vocabulary']; foreach ($tokens as $name => $original) { @@ -186,6 +195,7 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable break; } } + $language_manager->setConfigOverrideLanguage($original_language); } return $replacements; diff --git a/core/modules/taxonomy/tests/src/Functional/TokenReplaceTest.php b/core/modules/taxonomy/tests/src/Functional/TokenReplaceTest.php index 7474cee2d1..2940c55b68 100644 --- a/core/modules/taxonomy/tests/src/Functional/TokenReplaceTest.php +++ b/core/modules/taxonomy/tests/src/Functional/TokenReplaceTest.php @@ -5,8 +5,11 @@ use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Render\BubbleableMetadata; +use Drupal\system\Tests\Utility\AssertTokenReplacementTrait; /** + * Generates text using placeholders. + * * Generates text using placeholders for dummy content to check taxonomy token * replacement. * @@ -14,6 +17,8 @@ */ class TokenReplaceTest extends TaxonomyTestBase { + use AssertTokenReplacementTrait; + /** * The vocabulary used for creating terms. * @@ -33,10 +38,21 @@ class TokenReplaceTest extends TaxonomyTestBase { */ protected $defaultTheme = 'stark'; + /** + * Token service. + * + * @var \Drupal\Core\Utility\Token + */ + protected $tokenService; + + /** + * {@inheritdoc} + */ protected function setUp() { parent::setUp(); + $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 = [ @@ -65,12 +81,12 @@ protected function setUp() { * Creates some terms and a node, then tests the tokens generated from them. */ public 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 = []; @@ -88,13 +104,18 @@ public function testTaxonomyTokenReplacement() { $tests = []; $tests['[term:tid]'] = $term1->id(); $tests['[term:name]'] = $term1->getName(); - $tests['[term:description]'] = $term1->description->processed; + $tests['[term:description]'] = $term1->getDescription(); $tests['[term:url]'] = $term1->toUrl('canonical', ['absolute' => TRUE])->toString(); $tests['[term:node-count]'] = 0; + $tests['[term:parent]'] = '[term:parent]'; $tests['[term:parent:name]'] = '[term:parent:name]'; + $tests['[term:parent:url]'] = '[term:parent:url]'; $tests['[term:vocabulary:name]'] = $this->vocabulary->label(); $tests['[term:vocabulary]'] = $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.'); + $base_bubbleable_metadata = BubbleableMetadata::createFromObject($term1); $metadata_tests = []; @@ -103,37 +124,67 @@ public function testTaxonomyTokenReplacement() { $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; + $metadata_tests['[term:parent:url]'] = $base_bubbleable_metadata; $bubbleable_metadata = clone $base_bubbleable_metadata; $metadata_tests['[term:vocabulary:name]'] = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()); $metadata_tests['[term:vocabulary]'] = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()); - foreach ($tests as $input => $expected) { - $bubbleable_metadata = new BubbleableMetadata(); - $output = $token_service->replace($input, ['term' => $term1], ['langcode' => $language_interface->getId()], $bubbleable_metadata); - $this->assertEqual($output, $expected, new FormattableMarkup('Sanitized taxonomy term token %token replaced.', ['%token' => $input])); - $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); - } + $data = ['term' => $term1]; + $options = ['langcode' => $language_interface->getId()]; + $msg = 'Taxonomy term 1 token %token replaced with %output which is equal to %expected'; + $this->assertTokenReplacementAndCheckMetadata($tests, $data, $options, $msg, $metadata_tests); // Generate and test sanitized tokens for term2. $tests = []; $tests['[term:tid]'] = $term2->id(); $tests['[term:name]'] = $term2->getName(); - $tests['[term:description]'] = $term2->description->processed; + $tests['[term:description]'] = $term2->getDescription(); $tests['[term:url]'] = $term2->toUrl('canonical', ['absolute' => TRUE])->toString(); $tests['[term:node-count]'] = 1; + $tests['[term:vocabulary]'] = $this->vocabulary->label(); + $tests['[term:parent]'] = $term1->getName(); + $tests['[term:parent:tid]'] = $term1->id(); $tests['[term:parent:name]'] = $term1->getName(); + $tests['[term:parent:description]'] = $term1->getDescription(); $tests['[term:parent:url]'] = $term1->toUrl('canonical', ['absolute' => TRUE])->toString(); + $tests['[term:parent:node-count]'] = 0; $tests['[term:parent:parent:name]'] = '[term:parent:parent:name]'; $tests['[term:vocabulary:name]'] = $this->vocabulary->label(); + $tests['[term:parent:vocabulary]'] = $this->vocabulary->label(); + $tests['[term:parent:vocabulary:name]'] = $this->vocabulary->label(); // Test to make sure that we generated something for each token. $this->assertNotContains(0, array_map('strlen', $tests), 'No empty tokens generated.'); - foreach ($tests as $input => $expected) { - $output = $token_service->replace($input, ['term' => $term2], ['langcode' => $language_interface->getId()]); - $this->assertEqual($output, $expected, new FormattableMarkup('Sanitized taxonomy term token %token replaced.', ['%token' => $input])); - } + $base_bubbleable_metadata = BubbleableMetadata::createFromObject($term2); + $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; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $bubbleable_metadata = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()); + $metadata_tests['[term:vocabulary]'] = $bubbleable_metadata; + $metadata_tests['[term:vocabulary:name]'] = $bubbleable_metadata; + $base_bubbleable_metadata = BubbleableMetadata::createFromObject($term1)->addCacheTags($term2->getCacheTags()); + $metadata_tests['[term:parent]'] = $base_bubbleable_metadata; + $metadata_tests['[term:parent:tid]'] = $base_bubbleable_metadata; + $metadata_tests['[term:parent:name]'] = $base_bubbleable_metadata; + $metadata_tests['[term:parent:description]'] = $base_bubbleable_metadata; + $metadata_tests['[term:parent:url]'] = $base_bubbleable_metadata; + $metadata_tests['[term:parent:node-count]'] = $base_bubbleable_metadata; + $metadata_tests['[term:parent:parent:name]'] = $base_bubbleable_metadata; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $bubbleable_metadata = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()); + $metadata_tests['[term:parent:vocabulary:name]'] = $bubbleable_metadata; + $metadata_tests['[term:parent:vocabulary]'] = $bubbleable_metadata; + + $data = ['term' => $term2]; + $msg = 'Taxonomy term 2 token %token replaced with %output which is equal to %expected'; + $this->assertTokenReplacementAndCheckMetadata($tests, $data, $options, $msg, $metadata_tests); // Generate and test sanitized tokens. $tests = []; @@ -146,10 +197,18 @@ public function testTaxonomyTokenReplacement() { // Test to make sure that we generated something for each token. $this->assertNotContains(0, array_map('strlen', $tests), 'No empty tokens generated.'); - foreach ($tests as $input => $expected) { - $output = $token_service->replace($input, ['vocabulary' => $this->vocabulary], ['langcode' => $language_interface->getId()]); - $this->assertEqual($output, $expected, new FormattableMarkup('Sanitized taxonomy vocabulary token %token replaced.', ['%token' => $input])); - } + $base_bubbleable_metadata = BubbleableMetadata::createFromObject($this->vocabulary); + + $metadata_tests = []; + $metadata_tests['[vocabulary:vid]'] = $base_bubbleable_metadata; + $metadata_tests['[vocabulary:name]'] = $base_bubbleable_metadata; + $metadata_tests['[vocabulary:description]'] = $base_bubbleable_metadata; + $metadata_tests['[vocabulary:node-count]'] = $base_bubbleable_metadata; + $metadata_tests['[vocabulary:term-count]'] = $base_bubbleable_metadata; + + $data = ['vocabulary' => $this->vocabulary]; + $msg = 'Taxonomy vocabulary token %token replaced with %output which is equal to %expected'; + $this->assertTokenReplacementAndCheckMetadata($tests, $data, $options, $msg, $metadata_tests); } } diff --git a/core/modules/taxonomy/tests/src/Traits/TaxonomyTestTrait.php b/core/modules/taxonomy/tests/src/Traits/TaxonomyTestTrait.php index de4dce2449..8a783420ce 100644 --- a/core/modules/taxonomy/tests/src/Traits/TaxonomyTestTrait.php +++ b/core/modules/taxonomy/tests/src/Traits/TaxonomyTestTrait.php @@ -15,11 +15,15 @@ trait TaxonomyTestTrait { /** * 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 - * A vocabulary used for testing. + * The new taxonomy vocabulary object. + * @throws \Drupal\Core\Entity\EntityStorageException */ - public function createVocabulary() { - $vocabulary = Vocabulary::create([ + public function createVocabulary(array $values = []) { + $vocabulary = Vocabulary::create($values + [ 'name' => $this->randomMachineName(), 'description' => $this->randomMachineName(), 'vid' => mb_strtolower($this->randomMachineName()),