commit 057968adddb3135bcc1ef60a9224c076eec737df
Author: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date:   Wed Jul 20 15:36:16 2016 +1000

    [#CMS206] Patch for chained tokens
    
    [#CMS206] Remove patch

diff --git a/tests/src/Kernel/FieldTest.php b/tests/src/Kernel/FieldTest.php
index d418088..ace6d8e 100644
--- a/tests/src/Kernel/FieldTest.php
+++ b/tests/src/Kernel/FieldTest.php
@@ -12,6 +12,10 @@
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
 use Drupal\contact\Entity\Message;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\taxonomy\Tests\TaxonomyTestTrait;
+use Drupal\Core\Render\BubbleableMetadata;
 
 /**
  * Tests field tokens.
@@ -20,17 +24,27 @@
  */
 class FieldTest extends KernelTestBase {
 
+  use TaxonomyTestTrait;
+
   /**
    * @var \Drupal\filter\FilterFormatInterface
    */
   protected $testFormat;
 
+
+  /**
+   * Vocabulary for testing chained token support.
+   *
+   * @var \Drupal\taxonomy\VocabularyInterface
+   */
+  protected $vocabulary;
+
   /**
    * Modules to enable.
    *
    * @var array
    */
-  public static $modules = ['node', 'text', 'field', 'filter', 'contact', 'options'];
+  public static $modules = ['node', 'text', 'field', 'filter', 'contact', 'options', 'taxonomy'];
 
   /**
    * {@inheritdoc}
@@ -40,6 +54,7 @@ public function setUp() {
 
     $this->installEntitySchema('user');
     $this->installEntitySchema('node');
+    $this->installEntitySchema('taxonomy_term');
 
     // Create the article content type with a text field.
     $node_type = NodeType::create([
@@ -107,6 +122,64 @@ public function setUp() {
       'entity_type' => 'node',
       'bundle' => 'article',
     ])->save();
+
+    // Add a node reference field.
+    $reference_storage = FieldStorageConfig::create([
+      'field_name' => 'test_reference',
+      'entity_type' => 'node',
+      'type' => 'entity_reference',
+    ]);
+    $reference_storage->save();
+
+    $reference = FieldConfig::create([
+      'field_name' => 'test_reference',
+      'entity_type' => 'node',
+      'bundle' => 'article',
+      'label' => 'Test reference',
+    ]);
+    $reference->save();
+
+    // Add a taxonomy term reference field.
+    $this->vocabulary = $this->createVocabulary();
+    $reference_storage = FieldStorageConfig::create([
+      'field_name' => 'test_term_reference',
+      'entity_type' => 'node',
+      'type' => 'entity_reference',
+      'settings' => [
+        'target_type' => 'taxonomy_term',
+      ],
+    ]);
+    $reference_storage->save();
+
+    $reference = FieldConfig::create([
+      'field_name' => 'test_term_reference',
+      'entity_type' => 'node',
+      'bundle' => 'article',
+      'label' => 'Test term reference',
+      'settings' => [
+        'handler' => 'default:taxonomy_term',
+        'handler_settings' => [
+          'target_bundles' => [
+            $this->vocabulary->id() => $this->vocabulary->id(),
+          ],
+        ]
+      ]
+    ]);
+    $reference->save();
+
+    // Add a field to the vocabulary.
+    $storage = FieldStorageConfig::create([
+      'field_name' => 'term_field',
+      'entity_type' => 'taxonomy_term',
+      'type' => 'text',
+    ]);
+    $storage->save();
+    $field = FieldConfig::create([
+      'field_name' => 'term_field',
+      'entity_type' => 'taxonomy_term',
+      'bundle' => $this->vocabulary->id(),
+    ]);
+    $field->save();
   }
 
   /**
@@ -316,4 +389,117 @@ public function testBaseFieldTokens() {
     // Verify that node entity type doesn't have a uid token.
     $this->assertNull($tokenService->getTokenInfo('node', 'uid'));
   }
+
+  /*
+   * Tests chaining tokens.
+   */
+  public function testChainedTokens() {
+    $reference = Node::create([
+      'title' => 'Test node to reference',
+      'type' => 'article',
+      'test_field' => [
+        'value' => 'foo',
+        'format' => $this->testFormat->id(),
+      ]
+    ]);
+    $reference->save();
+    $term_reference_field_value = $this->randomString();
+    $term_reference = $this->createTerm($this->vocabulary, [
+      'name' => 'Term to reference',
+      'term_field' => [
+        'value' => $term_reference_field_value,
+        'format' => $this->testFormat->id(),
+      ],
+    ]);
+    $entity = Node::create([
+      'title' => 'Test entity reference',
+      'type' => 'article',
+      'test_reference' => ['target_id' => $reference->id()],
+      'test_term_reference' => ['target_id' => $term_reference->id()],
+    ]);
+    $entity->save();
+
+    $this->assertTokens('node', ['node' => $entity], [
+      'test_reference:entity:title' => Markup::create('Test node to reference'),
+      'test_reference:entity:test_field' => Markup::create('foo'),
+      'test_term_reference:entity:term_field' => Html::escape($term_reference_field_value),
+      'test_reference:target_id' => $reference->id(),
+      'test_term_reference:target_id' => $term_reference->id(),
+    ]);
+
+    // Test the :entity tokens.
+    $input = $this->mapTokenNames('node', ['test_term_reference:entity']);
+    $bubbleable_metadata = new BubbleableMetadata();
+    $replacement = \Drupal::token()->generate('node', $input, ['node' => $entity], [], $bubbleable_metadata);
+    $replacement_term_field_value = $replacement['[node:test_term_reference:entity]']->get('term_field')->getValue()[0]['value'];
+    $this->assertTrue($replacement_term_field_value == $term_reference_field_value);
+
+    // Test some non existent tokens.
+    $this->assertNoTokens('node', ['node' => $entity], [
+      'test_reference:1:title',
+      'test_term_reference:do_not_exists',
+      'test_term_reference:do:not:exists',
+      'test_term_reference:do_not_exists:0',
+    ]);
+  }
+
+  /**
+   * Tests support for cardinality > 1 chaining.
+   */
+  public function testMultivalueCardinality() {
+    /** @var \Drupal\field\FieldStorageConfigInterface $storage */
+    $storage = FieldStorageConfig::load('node.test_term_reference');
+    $storage->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+    $storage->save();
+
+    // Add a few terms.
+    $terms = [];
+    $terms_value = [];
+    foreach (range(1, 3) as $i) {
+      $terms_value[$i] = $this->randomString();
+      $terms[$i] = $this->createTerm($this->vocabulary, [
+        'name' => $this->randomString(),
+        'term_field' => [
+          'value' => $terms_value[$i],
+          'format' => $this->testFormat->id(),
+        ],
+      ]);
+    }
+
+    $entity = Node::create([
+      'title' => 'Test multivalue chained tokens',
+      'type' => 'article',
+      'test_term_reference' => [
+        ['target_id' => $terms[1]->id()],
+        ['target_id' => $terms[2]->id()],
+        ['target_id' => $terms[3]->id()],
+      ],
+    ]);
+    $entity->save();
+
+    $this->assertTokens('node', ['node' => $entity], [
+      'test_term_reference:0:entity:term_field' => Html::escape($terms[1]->term_field->value),
+      'test_term_reference:1:entity:term_field' => Html::escape($terms[2]->term_field->value),
+      'test_term_reference:2:entity:term_field' => Html::escape($terms[3]->term_field->value),
+      'test_term_reference:0:target_id' => $terms[1]->id(),
+      'test_term_reference:1:target_id' => $terms[2]->id(),
+      'test_term_reference:2:target_id' => $terms[3]->id(),
+    ]);
+
+    // Test some non existent tokens.
+    $this->assertNoTokens('node', ['node' => $entity], [
+      'test_term_reference:3:term_field',
+      'test_term_reference:0:do_not_exists',
+      'test_term_reference:1:do:not:exists',
+      'test_term_reference:1:2:do_not_exists',
+    ]);
+
+    // Test the :entity tokens.
+    $input = $this->mapTokenNames('node', ['test_term_reference:0:entity']);
+    $bubbleable_metadata = new BubbleableMetadata();
+    $replacement = \Drupal::token()->generate('node', $input, ['node' => $entity], [], $bubbleable_metadata);
+    $replacement_term_field_value = $replacement['[node:test_term_reference:0:entity]']->get('term_field')->getValue()[0]['value'];
+    $this->assertTrue($replacement_term_field_value == $terms_value[1]);
+  }
+
 }
diff --git a/token.tokens.inc b/token.tokens.inc
index 68e2755..1b964e1 100644
--- a/token.tokens.inc
+++ b/token.tokens.inc
@@ -5,6 +5,7 @@
  * Token callbacks for the token module.
  */
 use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Render\Element;
@@ -13,6 +14,7 @@
 use Drupal\Component\Utility\Unicode;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Xss;
+use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
 use Drupal\Core\Url;
 use Drupal\field\FieldConfigInterface;
 use Drupal\field\FieldStorageConfigInterface;
@@ -1276,6 +1278,16 @@ function field_token_info_alter(&$info) {
             'module' => 'token',
           ];
         }
+        elseif (($property_definition instanceof DataReferenceDefinitionInterface) && ($property_definition->getTargetDefinition() instanceof EntityDataDefinitionInterface)) {
+          $referenced_entity_type = $property_definition->getTargetDefinition()->getEntityTypeId();
+          $referenced_token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($referenced_entity_type);
+          $info['tokens'][$field_token_name][$property] = [
+            'name' => 'Referenced entity',
+            'description' => t('The entity referenced by this entity reference field.'),
+            'module' => 'token',
+            'type' => $referenced_token_type,
+          ];
+        }
       }
     }
   }
@@ -1345,7 +1357,7 @@ function field_tokens($type, $tokens, array $data = array(), array $options = ar
         list($field_name, $delta) = explode(':', $name, 2);
         if (!is_numeric($delta)) {
           unset($delta);
-         }
+        }
         $token_name = $field_name;
       }
       if (_token_module($data['token_type'], $token_name) != 'token') {
@@ -1395,12 +1407,67 @@ function field_tokens($type, $tokens, array $data = array(), array $options = ar
       }
       // Handle [entity:field_name:value] and [entity:field_name:0:value]
       // tokens.
-      else if ($field_tokens = \Drupal::token()->findWithPrefix($tokens, $field_name)) {
-        $property_token_data = [
-          'field_property' => TRUE,
-          $data['entity_type'] . '-' . $field_name => $data['entity']->$field_name,
-          'field_name' => $data['entity_type'] . '-' . $field_name,
-        ];
+      else {
+        $field = $entity->$field_name;
+        $parts = explode(':', $name, 4);
+        $multivalued = false;
+        $delta = 0;
+        if ((count($parts) >= 3) && is_numeric($parts[1])) {
+          // Multivalued field.
+          $multivalued = true;
+          $delta = $parts[1];
+        }
+        // Handle entity reference fields.
+        if (
+          $field->getFieldDefinition()->getType() === 'entity_reference'
+          && (count($parts) > 2)
+          && !((count($parts) == 3) && $multivalued)
+        ) {
+          if ((count($parts) >= 4) && ($parts[2] === 'entity')) {
+            // Token is [entity:field_name:0:entity:value].
+            if (count($field) <= $delta) {
+              // No such delta for this field, abort token replacement.
+              continue;
+            }
+            $literal_text = $parts[3];
+          }
+          elseif (count($parts) == 3 && ($parts[1] === 'entity')) {
+            // Token is [entity:field_name:entity:value].
+            $literal_text = $parts[2];
+          }
+          else {
+            // Token not of the form expected.
+            continue;
+          }
+
+          // Get the referenced entity type.
+          $field_storage_definition = $field->getFieldDefinition()->getFieldStorageDefinition();
+          $field_property_definition = $field_storage_definition->getPropertyDefinitions();
+          $referenced_token_type = $field_property_definition['entity']->getTargetDefinition()->getEntityTypeId();
+
+          $field_name = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($referenced_token_type, TRUE);
+          $referenced_entity = $field[$delta]->entity;
+          $bubbleable_metadata->addCacheableDependency($referenced_entity);
+
+          $field_tokens = [
+            $literal_text => $original,
+          ];
+          $property_token_data = [
+            $field_name => $referenced_entity,
+          ];
+        }
+        // Handle fields that are not entity reference fields or field property
+        // tokens of entity reference fields (eg. [entity:field_name:entity] or
+        // [entity:field_name:0:entity]).
+        else {
+          $field_tokens = \Drupal::token()->findWithPrefix($tokens, $field_name);
+          $property_token_data = [
+            'field_property' => TRUE,
+            $data['entity_type'] . '-' . $field_name => $data['entity']->$field_name,
+            'field_name' => $data['entity_type'] . '-' . $field_name,
+          ];
+        }
+
         $replacements += \Drupal::token()->generate($field_name, $field_tokens, $property_token_data, $options, $bubbleable_metadata);
       }
     }

commit 4b75f6fc3cd280301b5991442731dfeceb526c91
Author: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date:   Wed Jul 20 15:53:31 2016 +1000

    [#CMS206] Further patch token
    
    [#CMS206] And a test

diff --git a/tests/src/Kernel/FieldTest.php b/tests/src/Kernel/FieldTest.php
index ace6d8e..b9a7d88 100644
--- a/tests/src/Kernel/FieldTest.php
+++ b/tests/src/Kernel/FieldTest.php
@@ -425,6 +425,7 @@ public function testChainedTokens() {
       'test_term_reference:entity:term_field' => Html::escape($term_reference_field_value),
       'test_reference:target_id' => $reference->id(),
       'test_term_reference:target_id' => $term_reference->id(),
+      'test_term_reference:entity:url:path' => '/' . $term_reference->toUrl('canonical')->getInternalPath(),
     ]);
 
     // Test the :entity tokens.
diff --git a/token.tokens.inc b/token.tokens.inc
index 1b964e1..4f519e7 100644
--- a/token.tokens.inc
+++ b/token.tokens.inc
@@ -1431,9 +1431,9 @@ function field_tokens($type, $tokens, array $data = array(), array $options = ar
             }
             $literal_text = $parts[3];
           }
-          elseif (count($parts) == 3 && ($parts[1] === 'entity')) {
+          elseif (count($parts) >= 3 && ($parts[1] === 'entity')) {
             // Token is [entity:field_name:entity:value].
-            $literal_text = $parts[2];
+            $literal_text = implode(':', array_slice($parts, 2));
           }
           else {
             // Token not of the form expected.
