diff --git a/core/COPYRIGHT.txt b/core/COPYRIGHT.txt
index 1c4ad0e..1e01b38 100644
--- a/core/COPYRIGHT.txt
+++ b/core/COPYRIGHT.txt
@@ -47,6 +47,8 @@ Javascript
 
   jQuery Once - Copyright (c) 2009 Konstantin Käfer
 
+  jQuery treeTable - Copyright (c) 2010, Ludo van den Boom
+
   jQuery UI - Copyright (c) 2012 by the original authors
     (http://jqueryui.com/about)
 
diff --git a/core/misc/arrow-down.png b/core/misc/arrow-down.png
new file mode 100644
index 0000000..2edbb17
--- /dev/null
+++ b/core/misc/arrow-down.png
@@ -0,0 +1,4 @@
+‰PNG
+
+   IHDR         H%v?   PLTEÿÿÿ···kR%»   tRNS @æØf   IDATxœc`@õì0È`ào``g``‚
+ KzHƒ“Ï    IEND®B`‚
\ No newline at end of file
diff --git a/core/misc/arrow-right.png b/core/misc/arrow-right.png
new file mode 100644
index 0000000..6f2f482
--- /dev/null
+++ b/core/misc/arrow-right.png
@@ -0,0 +1,3 @@
+‰PNG
+
+   IHDR         rëä|   sBIT|dˆ   6IDAT(‘cøÿÿ?©«àöíÛÿ“¥	ŸF¼špi$¨	›F¢4¡k$Z²FÚÙDûÐ£}Š „Óþ‘¿Ö÷é    IEND®B`‚
\ No newline at end of file
diff --git a/core/misc/jquery.treeTable.css b/core/misc/jquery.treeTable.css
new file mode 100644
index 0000000..bec225a
--- /dev/null
+++ b/core/misc/jquery.treeTable.css
@@ -0,0 +1,44 @@
+
+/* jQuery TreeTable Core 2.0 stylesheet
+ *
+ * This file contains styles that are used to display the tree table. Each tree
+ * table is assigned the +treeTable+ class.
+ * ========================================================================= */
+
+/* jquery.treeTable.collapsible
+ * ------------------------------------------------------------------------- */
+.treeTable tr td .expander {
+  background-position: left center;
+  background-repeat: no-repeat;
+  cursor: pointer;
+  padding: 0;
+  zoom: 1; /* IE7 Hack */
+}
+
+.treeTable tr.collapsed td .expander {
+  background-image: url(arrow-right.png);
+}
+
+.treeTable tr.expanded td .expander {
+  background-image: url(arrow-down.png);
+}
+
+/* jquery.treeTable.sortable
+ * ------------------------------------------------------------------------- */
+.treeTable tr.selected, .treeTable tr.accept {
+  background-color: #3875d7;
+  color: #fff;
+}
+
+.treeTable tr.collapsed.selected td .expander, .treeTable tr.collapsed.accept td .expander {
+  background-image: url(../images/toggle-expand-light.png);
+}
+
+.treeTable tr.expanded.selected td .expander, .treeTable tr.expanded.accept td .expander {
+  background-image: url(../images/toggle-collapse-light.png);
+}
+
+.treeTable .ui-draggable-dragging {
+  color: #000;
+  z-index: 1;
+}
diff --git a/core/misc/jquery.treeTable.js b/core/misc/jquery.treeTable.js
new file mode 100644
index 0000000..847b8f3
--- /dev/null
+++ b/core/misc/jquery.treeTable.js
@@ -0,0 +1,220 @@
+
+/*
+ * jQuery treeTable Plugin 2.3.0
+ * http://ludo.cubicphuse.nl/jquery-plugins/treeTable/
+ *
+ * Copyright 2010, Ludo van den Boom
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ */
+(function($) {
+  // Helps to make options available to all functions
+  // TODO: This gives problems when there are both expandable and non-expandable
+  // trees on a page. The options shouldn't be global to all these instances!
+  var options;
+  var defaultPaddingLeft;
+
+  $.fn.treeTable = function(opts) {
+    options = $.extend({}, $.fn.treeTable.defaults, opts);
+
+    return this.each(function() {
+      $(this).addClass("treeTable").find("tbody tr").each(function() {
+        // Initialize root nodes only if possible
+        if(!options.expandable || $(this)[0].className.search(options.childPrefix) == -1) {
+          // To optimize performance of indentation, I retrieve the padding-left
+          // value of the first root node. This way I only have to call +css+
+          // once.
+          if (isNaN(defaultPaddingLeft)) {
+            defaultPaddingLeft = parseInt($($(this).children("td")[options.treeColumn]).css('padding-left'), 10);
+          }
+
+          initialize($(this));
+        } else if(options.initialState == "collapsed") {
+          this.style.display = "none"; // Performance! $(this).hide() is slow...
+        }
+      });
+    });
+  };
+
+  $.fn.treeTable.defaults = {
+    childPrefix: "child-of-",
+    clickableNodeNames: false,
+    expandable: true,
+    indent: 19,
+    initialState: "collapsed",
+    treeColumn: 0
+  };
+
+  // Recursively hide all node's children in a tree
+  $.fn.collapse = function() {
+    $(this).addClass("collapsed");
+
+    childrenOf($(this)).each(function() {
+      if(!$(this).hasClass("collapsed")) {
+        $(this).collapse();
+      }
+
+      this.style.display = "none"; // Performance! $(this).hide() is slow...
+    });
+
+    return this;
+  };
+
+  // Recursively show all node's children in a tree
+  $.fn.expand = function() {
+    $(this).removeClass("collapsed").addClass("expanded");
+
+    childrenOf($(this)).each(function() {
+      initialize($(this));
+
+      if($(this).is(".expanded.parent")) {
+        $(this).expand();
+      }
+
+      // this.style.display = "table-row"; // Unfortunately this is not possible with IE :-(
+      $(this).show();
+    });
+
+    return this;
+  };
+
+  // Reveal a node by expanding all ancestors
+  $.fn.reveal = function() {
+    $(ancestorsOf($(this)).reverse()).each(function() {
+      initialize($(this));
+      $(this).expand().show();
+    });
+
+    return this;
+  };
+
+  // Add an entire branch to +destination+
+  $.fn.appendBranchTo = function(destination) {
+    var node = $(this);
+    var parent = parentOf(node);
+
+    var ancestorNames = $.map(ancestorsOf($(destination)), function(a) { return a.id; });
+
+    // Conditions:
+    // 1: +node+ should not be inserted in a location in a branch if this would
+    //    result in +node+ being an ancestor of itself.
+    // 2: +node+ should not have a parent OR the destination should not be the
+    //    same as +node+'s current parent (this last condition prevents +node+
+    //    from being moved to the same location where it already is).
+    // 3: +node+ should not be inserted as a child of +node+ itself.
+    if($.inArray(node[0].id, ancestorNames) == -1 && (!parent || (destination.id != parent[0].id)) && destination.id != node[0].id) {
+      indent(node, ancestorsOf(node).length * options.indent * -1); // Remove indentation
+
+      if(parent) { node.removeClass(options.childPrefix + parent[0].id); }
+
+      node.addClass(options.childPrefix + destination.id);
+      move(node, destination); // Recursively move nodes to new location
+      indent(node, ancestorsOf(node).length * options.indent);
+    }
+
+    return this;
+  };
+
+  // Add reverse() function from JS Arrays
+  $.fn.reverse = function() {
+    return this.pushStack(this.get().reverse(), arguments);
+  };
+
+  // Toggle an entire branch
+  $.fn.toggleBranch = function() {
+    if($(this).hasClass("collapsed")) {
+      $(this).expand();
+    } else {
+      $(this).removeClass("expanded").collapse();
+    }
+
+    return this;
+  };
+
+  // === Private functions
+
+  function ancestorsOf(node) {
+    var ancestors = [];
+    while(node = parentOf(node)) {
+      ancestors[ancestors.length] = node[0];
+    }
+    return ancestors;
+  };
+
+  function childrenOf(node) {
+    return $(node).siblings("tr." + options.childPrefix + node[0].id);
+  };
+
+  function getPaddingLeft(node) {
+    var paddingLeft = parseInt(node[0].style.paddingLeft, 10);
+    return (isNaN(paddingLeft)) ? defaultPaddingLeft : paddingLeft;
+  }
+
+  function indent(node, value) {
+    var cell = $(node.children("td")[options.treeColumn]);
+    cell[0].style.paddingLeft = getPaddingLeft(cell) + value + "px";
+
+    childrenOf(node).each(function() {
+      indent($(this), value);
+    });
+  };
+
+  function initialize(node) {
+    if(!node.hasClass("initialized")) {
+      node.addClass("initialized");
+
+      var childNodes = childrenOf(node);
+
+      if(!node.hasClass("parent") && childNodes.length > 0) {
+        node.addClass("parent");
+      }
+
+      if(node.hasClass("parent")) {
+        var cell = $(node.children("td")[options.treeColumn]);
+        var padding = getPaddingLeft(cell) + options.indent;
+
+        childNodes.each(function() {
+          $(this).children("td")[options.treeColumn].style.paddingLeft = padding + "px";
+        });
+
+        if(options.expandable) {
+          cell.prepend('<span style="margin-left: -' + options.indent + 'px; padding-left: ' + options.indent + 'px" class="expander"></span>');
+          $(cell[0].firstChild).click(function() { node.toggleBranch(); });
+
+          if(options.clickableNodeNames) {
+            cell[0].style.cursor = "pointer";
+            $(cell).click(function(e) {
+              // Don't double-toggle if the click is on the existing expander icon
+              if (e.target.className != 'expander') {
+                node.toggleBranch();
+              }
+            });
+          }
+
+          // Check for a class set explicitly by the user, otherwise set the default class
+          if(!(node.hasClass("expanded") || node.hasClass("collapsed"))) {
+            node.addClass(options.initialState);
+          }
+
+          if(node.hasClass("expanded")) {
+            node.expand();
+          }
+        }
+      }
+    }
+  };
+
+  function move(node, destination) {
+    node.insertAfter(destination);
+    childrenOf(node).reverse().each(function() { move($(this), node[0]); });
+  };
+
+  function parentOf(node) {
+    var classNames = node[0].className.split(' ');
+
+    for(key in classNames) {
+      if(classNames[key].match(options.childPrefix)) {
+        return $(node).siblings("#" + classNames[key].substring(options.childPrefix.length));
+      }
+    }
+  };
+})(jQuery);
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index ed4f0c2..d9fb396 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1999,6 +1999,19 @@ function system_library_info() {
     ),
   );
 
+  // jQuery treeTable plugin.
+  $libraries['jquery.treetable'] = array(
+    'title' => 'jQuery treeTable',
+    'website' => 'http://plugins.jquery.com/project/treetable',
+    'version' => '2.3.0',
+    'js' => array(
+      'core/misc/jquery.treeTable.js' => array('group' => JS_LIBRARY),
+    ),
+    'css' => array(
+      'core/misc/jquery.treeTable.css' => array(),
+    ),
+  );
+
   return $libraries;
 }
 
diff --git a/core/modules/token/token.info b/core/modules/token/token.info
new file mode 100644
index 0000000..de910b6
--- /dev/null
+++ b/core/modules/token/token.info
@@ -0,0 +1,5 @@
+name = Token
+description = Provides a user interface for tokens.
+package = Core
+version = VERSION
+core = 8.x
diff --git a/core/modules/token/token.js b/core/modules/token/token.js
new file mode 100644
index 0000000..8953c25
--- /dev/null
+++ b/core/modules/token/token.js
@@ -0,0 +1,85 @@
+/**
+ * @file
+ * JavaScript behaviors for the token selector tree.
+ */
+
+(function ($) {
+
+Drupal.behaviors.tokenTree = {
+  attach: function (context, settings) {
+    $('table.token-tree', context).once('token-tree', function () {
+      $(this).treeTable();
+    });
+  }
+};
+
+Drupal.behaviors.tokenDialog = {
+  attach: function (context, settings) {
+    $('a.token-dialog', context).once('token-dialog').click(function() {
+      var url = $(this).attr('href');
+      var dialog = $('<div style="display: none" class="loading">' + Drupal.t('Loading token browser...') + '</div>').appendTo('body');
+      dialog.dialog({
+        title: $(this).attr('title') || Drupal.t('Available tokens'),
+        width: 700,
+        close: function(event, ui) {
+          dialog.remove();
+        }
+      });
+      // Load the token tree using AJAX.
+      dialog.load(
+        url,
+        {},
+        function (responseText, textStatus, XMLHttpRequest) {
+          dialog.removeClass('loading');
+        }
+      );
+      // Prevent browser from following the link.
+      return false;
+    });
+  }
+}
+
+Drupal.behaviors.tokenInsert = {
+  attach: function (context, settings) {
+    // Keep track of which textfield was last selected/focused.
+    $('textarea, input[type="text"]', context).focus(function() {
+      Drupal.settings.tokenFocusedField = this;
+    });
+
+    $('.token-click-insert .token-key', context).once('token-click-insert', function() {
+      var newThis = $('<a href="javascript:void(0);" title="' + Drupal.t('Insert this token into your form') + '">' + $(this).html() + '</a>').click(function(){
+        if (typeof Drupal.settings.tokenFocusedField == 'undefined') {
+          alert(Drupal.t('First click a text field to insert your tokens into.'));
+        }
+        else {
+          var myField = Drupal.settings.tokenFocusedField;
+          var myValue = $(this).text();
+
+          //IE support
+          if (document.selection) {
+            myField.focus();
+            sel = document.selection.createRange();
+            sel.text = myValue;
+          }
+
+          //MOZILLA/NETSCAPE support
+          else if (myField.selectionStart || myField.selectionStart == '0') {
+            var startPos = myField.selectionStart;
+            var endPos = myField.selectionEnd;
+            myField.value = myField.value.substring(0, startPos)
+                          + myValue
+                          + myField.value.substring(endPos, myField.value.length);
+          } else {
+            myField.value += myValue;
+          }
+
+          $('html,body').animate({scrollTop: $(myField).offset().top}, 500);
+        }
+        return false;
+      });
+      $(this).html(newThis);
+    });
+  }
+};
+
+})(jQuery);
diff --git a/core/modules/token/token.module b/core/modules/token/token.module
new file mode 100644
index 0000000..6062fde
--- /dev/null
+++ b/core/modules/token/token.module
@@ -0,0 +1,408 @@
+<?php
+
+/**
+ * @file
+ * Provides a user interface for tokens.
+ */
+
+/**
+ * The maximum depth for token tree recursion.
+ */
+define('TOKEN_MAX_DEPTH', 9);
+
+/**
+ * Implements hook_help().
+ */
+function token_help($path, $arg) {
+  switch ($path) {
+    case 'admin/help#token':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Token module provides a token browser, which allows you to select tokens to insert into fields that accept tokens.') . '</p>';
+    return $output;
+  }
+}
+
+/**
+ * Implements hook_menu().
+ */
+function token_menu() {
+  $items['token/autocomplete/%token_type'] = array(
+    'page callback' => 'token_autocomplete_token',
+    'page arguments' => array(2),
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+    'file' => 'token.pages.inc',
+  );
+
+  $items['token/tree'] = array(
+    'page callback' => 'token_page_output_tree',
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+    'file' => 'token.pages.inc',
+  );
+
+  return $items;
+}
+
+/**
+ * Loads a token type for menu router items.
+ *
+ * @param string $token_type
+ *   String representing the type of token to load (generally the module or
+ *   entity name).
+ *
+ * @return array
+ *   Array containing the token information for this token type.
+ */
+function token_type_load($token_type) {
+  $info = token_get_info();
+  return isset($info['types'][$token_type]) ? $info['types'][$token_type] : FALSE;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function token_theme() {
+  $info['tree_table'] = array(
+    'variables' => array(
+      'header' => array(),
+      'rows' => array(),
+      'attributes' => array(),
+      'empty' => '',
+      'caption' => '',
+    ),
+    'file' => 'token.pages.inc',
+  );
+  $info['token_tree'] = array(
+    'variables' => array(
+      'token_types' => array(),
+      'global_types' => TRUE,
+      'click_insert' => TRUE,
+      'show_restricted' => FALSE,
+      'recursion_limit' => 3,
+      'dialog' => FALSE,
+    ),
+    'file' => 'token.pages.inc',
+  );
+  $info['token_tree_link'] = array(
+    'variables' => array(
+      'text' => NULL,
+      'options' => array(),
+      'dialog' => TRUE,
+    ),
+    'file' => 'token.pages.inc',
+  );
+
+  return $info;
+}
+
+/**
+ * Implements hook_library_info().
+ */
+function token_library_info() {
+  $libraries['token_dialog'] = array(
+    'title' => 'Token dialog',
+    'version' => '1.0',
+    'js' => array(
+      drupal_get_path('module', 'token') . '/token.js' => array(),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery.ui.dialog'),
+    ),
+  );
+
+  return $libraries;
+}
+
+/**
+ * Retrieves and sorts token information.
+ *
+ * @param string $token_type
+ *   (optional) A particular token type to return information for.
+ * @param string $token
+ *   (optional) A particular token name within $token_type to return
+ *   information for.
+ *
+ * @return array
+ *   An array of all token information from hook_token_info(), or the array
+ *   of a token type or specific token.
+ *
+ * @see hook_token_info()
+ * @see hook_token_info_alter()
+ */
+function token_get_info($token_type = NULL, $token = NULL) {
+  global $language;
+
+  // @todo This function needs to use database caching.
+
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['token_info'] = &drupal_static(__FUNCTION__);
+  }
+  $token_info = &$drupal_static_fast['token_info'];
+
+  if (empty($token_info)) {
+    $token_info = token_info();
+
+    foreach (array_keys($token_info['types']) as $type_key) {
+      if (isset($token_info['types'][$type_key]['type'])) {
+        $base_type = $token_info['types'][$type_key]['type'];
+        // If this token type extends another token type, then merge in
+        // the base token type's tokens.
+        if (isset($token_info['tokens'][$base_type])) {
+          $token_info['tokens'] += array($type_key => array());
+          $token_info['tokens'][$type_key] += $token_info['tokens'][$base_type];
+        }
+      }
+      else {
+        // Add a 'type' value to each token type so we can properly use
+        // token_type_load().
+        $token_info['types'][$type_key]['type'] = $type_key;
+      }
+    }
+
+    // Pre-sort tokens.
+    uasort($token_info['types'], 'token_asort_tokens');
+    foreach (array_keys($token_info['tokens']) as $type) {
+      uasort($token_info['tokens'][$type], 'token_asort_tokens');
+    }
+  }
+
+  if (isset($token_type) && isset($token)) {
+    return isset($token_info['tokens'][$token_type][$token]) ? $token_info['tokens'][$token_type][$token] : NULL;
+  }
+  elseif (isset($token_type)) {
+    return isset($token_info['types'][$token_type]) ? $token_info['types'][$token_type] : NULL;
+  }
+  else {
+    return $token_info;
+  }
+}
+
+/**
+ * Sorts tokens by the 'name' property.
+ *
+ * Callback for uasort() within token_get_info().
+ */
+function token_asort_tokens($token_a, $token_b) {
+  return strnatcmp($token_a['name'], $token_b['name']);
+}
+
+/**
+ * Gets a list of token types that can be used without any context (global).
+ *
+ * @return
+ *   An array of global token types.
+ */
+function token_get_global_token_types() {
+  $global_types = &drupal_static(__FUNCTION__, array());
+
+  // @todo This function needs to use database caching.
+
+  if (empty($global_types)) {
+    $token_info = token_get_info();
+    foreach ($token_info['types'] as $type => $type_info) {
+      // If the token types has not specified that 'needs-data' => TRUE, then
+      // it is a global token type that will always be replaced in any context.
+      if (empty($type_info['needs-data'])) {
+        $global_types[] = $type;
+      }
+    }
+  }
+
+  return $global_types;
+}
+
+/**
+ * Builds a tree array of tokens used for theming or information.
+ *
+ * @param string $token_type
+ *   The token type.
+ * @param array $options
+ *   Array containing options governing how the tree is built:
+ *   - restricted: TRUE to show restricted tokens; FALSE (default) to hide them.
+ *   - depth: Maximum depth to traverse in the token tree.
+ *   - flat: TRUE to return a flat array; FALSE (default) to return a tree.
+ *
+ * @return array
+ *    Array containing the tree of tokens.
+ */
+function token_build_tree($token_type, array $options = array()) {
+  global $language;
+
+  // Static cache of already built token trees.
+  $trees = &drupal_static(__FUNCTION__, array());
+
+  // @todo This function needs to use database caching.
+
+  $options += array(
+    'restricted' => FALSE,
+    'depth' => 4,
+    'data' => array(),
+    'values' => FALSE,
+    'flat' => FALSE,
+  );
+
+  // Do not allow past the maximum token information depth.
+  $options['depth'] = min($options['depth'], TOKEN_MAX_DEPTH);
+
+  // If $token_type is an entity, make sure we are using the actual token type.
+  if ($entity_token_type = token_get_entity_mapping('entity', $token_type)) {
+    $token_type = $entity_token_type;
+  }
+
+  $tree_cid = "tree:{$token_type}:{$language->language}:{$options['depth']}";
+
+  // If we do not have this base tree in the static cache, check {cache_token}
+  // otherwise generate and store it in the cache.
+  if (!isset($trees[$tree_cid])) {
+    $options['parents'] = array();
+    $trees[$tree_cid] = _token_build_tree($token_type, $options);
+  }
+
+  $tree = $trees[$tree_cid];
+
+  // If the user has requested a flat tree, convert it.
+  if (!empty($options['flat'])) {
+    $tree = token_flatten_tree($tree);
+  }
+
+  // Fill in token values.
+  if (!empty($options['values'])) {
+    $token_values = array();
+    foreach ($tree as $token => $token_info) {
+      if (!empty($token_info['dynamic']) || !empty($token_info['restricted'])) {
+        continue;
+      }
+      elseif (!isset($token_info['value'])) {
+        $token_values[$token_info['token']] = $token;
+      }
+    }
+    if (!empty($token_values)) {
+      $token_values = token_generate($token_type, $token_values, $options['data']);
+      foreach ($token_values as $token => $replacement) {
+        $tree[$token]['value'] = $replacement;
+      }
+    }
+  }
+
+  return $tree;
+}
+
+/**
+ * Flattens a token tree.
+ *
+ * @param array $tree
+ *   The tree to flatten.
+ *
+ * @return array
+ *   The flattened tree.
+ */
+function token_flatten_tree($tree) {
+  $result = array();
+  foreach ($tree as $token => $token_info) {
+    $result[$token] = $token_info;
+    if (isset($token_info['children']) && is_array($token_info['children'])) {
+      $result += token_flatten_tree($token_info['children']);
+    }
+  }
+  return $result;
+}
+
+/**
+ * Generates a token tree.
+ */
+function _token_build_tree($token_type, array $options) {
+  $options += array(
+    'parents' => array(),
+  );
+
+  $info = token_get_info();
+  if ($options['depth'] <= 0 || !isset($info['types'][$token_type]) || !isset($info['tokens'][$token_type])) {
+    return array();
+  }
+
+  $tree = array();
+  foreach ($info['tokens'][$token_type] as $token => $token_info) {
+    // Build the raw token string.
+    $token_parents = $options['parents'];
+    if (empty($token_parents)) {
+      // If the parents array is currently empty, assume the token type is its
+      // parent.
+      $token_parents[] = $token_type;
+    }
+    elseif (in_array($token, array_slice($token_parents, 1))) {
+      // Prevent duplicate recursive tokens. For example, this will prevent
+      // the tree from generating the following tokens or deeper:
+      // [comment:parent:parent]
+      // [comment:parent:root:parent]
+      continue;
+    }
+
+    $token_parents[] = $token;
+    if (!empty($token_info['dynamic'])) {
+      $token_parents[] = '?';
+    }
+    $raw_token = '[' . implode(':', $token_parents) . ']';
+    $tree[$raw_token] = $token_info;
+    $tree[$raw_token]['raw token'] = $raw_token;
+
+    // Add the token's real name (leave out the base token type).
+    $tree[$raw_token]['token'] = implode(':', array_slice($token_parents, 1));
+
+    // Add the token's parent as its raw token value.
+    if (!empty($options['parents'])) {
+      $tree[$raw_token]['parent'] = '[' . implode(':', $options['parents']) . ']';
+    }
+
+    // Fetch the child tokens.
+    if (!empty($token_info['type'])) {
+      $child_options = $options;
+      $child_options['depth']--;
+      $child_options['parents'] = $token_parents;
+      $tree[$raw_token]['children'] = _token_build_tree($token_info['type'], $child_options);
+    }
+  }
+
+  return $tree;
+}
+
+/**
+ * Returns all entity type to token type mappings, or looks up one mapping.
+ *
+ * This is needed especially for taxonomy-related tokens, which do not
+ * use the entity name as the token type name.
+ *
+ * @param string $lookup_type
+ *   (optional) Type of value to search for, either 'token' or 'entity'.
+ * @param string $lookup_value
+ *   (optional) Value to search for (a token type or entity string). For
+ *   instance, if $lookup_type is 'token', $lookup_value gives a token type
+ *   that you want to find the entity type for.
+ *
+ * @return array|string|null
+ *   Array of all mappings of entity type to the corresponding token type, or
+ *   if both $lookup_type and $lookup_value are provided, the corresponding
+ *   mapping (or NULL if not found).
+ */
+function token_get_entity_mapping($lookup_type = 'token', $lookup_value = NULL) {
+  $mapping = &drupal_static(__FUNCTION__, array());
+
+  if (empty($mapping)) {
+    foreach (entity_get_info() as $entity_type => $info) {
+      $mapping[$entity_type] = !empty($info['token type']) ? $info['token type'] : $entity_type;
+    }
+  }
+
+  if (!isset($lookup_value)) {
+    return $mapping;
+  }
+  elseif ($lookup_type == 'token') {
+    return array_search($lookup_value, $mapping);
+  }
+  elseif ($lookup_type == 'entity') {
+    return isset($mapping[$value]) ? $mapping[$value] : FALSE;
+  }
+}
diff --git a/core/modules/token/token.pages.inc b/core/modules/token/token.pages.inc
new file mode 100644
index 0000000..e5a22ee
--- /dev/null
+++ b/core/modules/token/token.pages.inc
@@ -0,0 +1,326 @@
+<?php
+
+/**
+ * @file
+ * User page callbacks for the token module.
+ */
+
+/**
+ * Returns HTML for a link to a token tree or token tree dialog.
+ *
+ * @ingroup themeable
+ */
+function theme_token_tree_link($variables) {
+  if (empty($variables['text'])) {
+    $variables['text'] = t('Browse available tokens.');
+  }
+
+  if (!empty($variables['dialog'])) {
+    drupal_add_library('token', 'token_dialog');
+    $variables['options']['attributes']['class'][] = 'token-dialog';
+  }
+
+  $info = token_theme();
+  $variables['options']['query']['options'] = array_intersect_key($variables, $info['token_tree']['variables']);
+
+  // We should never pass the dialog option to theme_token_tree(). It is only
+  // used for this function.
+  unset($variables['options']['query']['options']['dialog']);
+
+  // Add a security token so that the tree page should only work when used
+  // when the dialog link is output with theme('token_tree_link').
+  $variables['options']['query']['token'] = drupal_get_token('token-tree:' . serialize($variables['options']['query']['options']));
+
+  // Because PHP converts query strings with arrays into a different syntax on
+  // the next request, the options have to be encoded with JSON in the query
+  // string so that we can reliably decode it for token comparison.
+  $variables['options']['query']['options'] = drupal_json_encode($variables['options']['query']['options']);
+
+  // Set the token tree to open in a separate window.
+  $variables['options']['attributes'] + array('target' => '_blank');
+
+  return l($variables['text'], 'token/tree', $variables['options']);
+}
+
+/**
+ * Page callback: Outputs a token tree on its own page.
+ */
+function token_page_output_tree() {
+  $options = isset($_GET['options']) ? drupal_json_decode($_GET['options']) : array();
+
+  // Check the token against the serialized options to prevent random access to
+  // the token browser page.
+  if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], 'token-tree:' . serialize($options))) {
+    return MENU_ACCESS_DENIED;
+  }
+
+  // Force the dialog option to be false so we're not creating a dialog within
+  // a dialog.
+  $options['dialog'] = FALSE;
+
+  $output = theme('token_tree', $options);
+  print '<html><head><title></title>' . drupal_get_css() . drupal_get_js() . '</head>';
+  print '<body class="token-tree">' . $output . '</body></html>';
+  drupal_exit();
+}
+
+/**
+ * Returns HTML for a token tree table.
+ *
+ * @ingroup themeable
+ */
+function theme_tree_table($variables) {
+  foreach ($variables['rows'] as &$row) {
+    $row += array('class' => array());
+    if (!empty($row['parent'])) {
+      $row['class'][] = 'child-of-' . $row['parent'];
+      unset($row['parent']);
+    }
+  }
+
+  if (!empty($variables['rows'])) {
+    drupal_add_library('system', 'jquery.treetable');
+  }
+
+  return theme('table', $variables);
+}
+
+/**
+ * Returns HTML for a tree display of nested tokens or a link to the dialog.
+ *
+ * @ingroup themeable
+ */
+function theme_token_tree($variables) {
+  if (!empty($variables['dialog'])) {
+    return theme('token_tree_link', $variables);
+  }
+
+  $token_types = $variables['token_types'];
+  $info = token_get_info();
+
+  if ($token_types == 'all') {
+    $token_types = array_keys($info['types']);
+  }
+  elseif ($variables['global_types']) {
+    $token_types = array_merge($token_types, token_get_global_token_types());
+  }
+
+  $element = array(
+    '#cache' => array(
+      'cid' => 'tree-rendered:' . hash('sha256', serialize(array('token_types' => $token_types, 'global_types' => NULL) + $variables)),
+      'bin' => 'cache_token',
+    ),
+  );
+
+  $options = array(
+    'flat' => TRUE,
+    'restricted' => $variables['show_restricted'],
+    'depth' => $variables['recursion_limit'],
+  );
+  $multiple_token_types = (count($token_types) > 1);
+  $rows = array();
+
+  foreach ($info['types'] as $type => $type_info) {
+    if (!in_array($type, $token_types)) {
+      continue;
+    }
+
+    if ($multiple_token_types) {
+      $row = _token_token_tree_format_row($type, $type_info, TRUE);
+      unset($row['data']['value']);
+      $rows[] = $row;
+    }
+
+    $tree = token_build_tree($type, $options);
+    foreach ($tree as $token => $token_info) {
+      if (!empty($token_info['restricted']) && empty($variables['show_restricted'])) {
+        continue;
+      }
+      if ($multiple_token_types && !isset($token_info['parent'])) {
+        $token_info['parent'] = $type;
+      }
+      $row = _token_token_tree_format_row($token, $token_info);
+      unset($row['data']['value']);
+      $rows[] = $row;
+    }
+  }
+
+  $element += array(
+    '#theme' => 'tree_table',
+    '#header' => array(
+      t('Name'),
+      t('Token'),
+      t('Description'),
+    ),
+    '#rows' => $rows,
+    '#attributes' => array('class' => array('token-tree')),
+    '#empty' => t('No tokens available'),
+    '#attached' => array(
+      'js' => array(drupal_get_path('module', 'token') . '/token.js'),
+      'css' => array(drupal_get_path('module', 'token') . '/token.css'),
+      'library' => array(array('token', 'treeTable')),
+    ),
+  );
+
+  if ($variables['click_insert']) {
+    $element['#caption'] = t('Click a token to insert it into the field you\'ve last clicked.');
+    $element['#attributes']['class'][] = 'token-click-insert';
+  }
+
+  $output = drupal_render($element);
+  return $output;
+}
+
+/**
+ * Builds a row in the token tree.
+ */
+function _token_token_tree_format_row($token, array $token_info, $is_group = FALSE) {
+  // Build a statically cached array of default values. This is around four
+  // times as efficient as building the base array from scratch each time this
+  // function is called.
+  static $defaults = array(
+    'id' => '',
+    'class' => array(),
+    'data' => array(
+      'name' => '',
+      'token' => '',
+      'value' => '',
+      'description' => '',
+    ),
+  );
+
+  $row = $defaults;
+  $row['id'] = _token_clean_css_identifier($token);
+  $row['data']['name'] = $token_info['name'];
+  $row['data']['description'] = $token_info['description'];
+
+  if ($is_group) {
+    // This is a token type/group.
+    $row['class'][] = 'token-group';
+  }
+  else {
+    // This is a token.
+    $row['data']['token']['data'] = $token;
+    $row['data']['token']['class'][] = 'token-key';
+    if (isset($token_info['value'])) {
+      $row['data']['value'] = $token_info['value'];
+    }
+    if (!empty($token_info['parent'])) {
+      $row['parent'] = _token_clean_css_identifier($token_info['parent']);
+    }
+  }
+
+  return $row;
+}
+
+/**
+ * Makes a CSS identifier for tokens.
+ *
+ * Trims brackets from the token and changes colon to hyphen.
+ *
+ * @see drupal_clean_css_identifier()
+ */
+function _token_clean_css_identifier($id) {
+  static $replacements = array(' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => '', ':' => '--', '?' => '', '<' => '-', '>' => '-');
+  return 'token-' . rtrim(strtr(trim($id, '[]'), $replacements), '-');
+}
+
+/**
+ * Page callback: Returns auto-complete options for tokens.
+ */
+function token_autocomplete() {
+  $args = func_get_args();
+  $string = implode('/', $args);
+
+  $token_info = token_info();
+
+  preg_match_all('/\[([^\s\]:]*):?([^\s\]]*)?\]?/', $string, $matches);
+  $types = $matches[1];
+  $tokens = $matches[2];
+
+  foreach ($types as $index => $type) {
+    if (!empty($tokens[$index]) || isset($token_info['types'][$type])) {
+      token_autocomplete_token($type, $tokens[$index]);
+    }
+    else {
+      token_autocomplete_type($type);
+    }
+  }
+
+}
+
+/**
+ * Outputs the best token type matches for the given string in JSON format.
+ *
+ * There is no return value (output is sent to the browser). The output is
+ * an array of the closest matches between the input string and existing
+ * token types, in JSON format.
+ *
+ * @param string $string
+ *   String to match.
+ */
+function token_autocomplete_type($string = '') {
+  $token_info = token_info();
+  $types = $token_info['types'];
+  $matches = array();
+
+  foreach ($types as $type => $info) {
+    if (!$string || strpos($type, $string) === 0) {
+      $type_key = "[{$type}:";
+      $matches[$type_key] = levenshtein($type, $string);
+    }
+  }
+
+  if ($string) {
+    asort($matches);
+  }
+  else {
+    ksort($matches);
+  }
+
+  $matches = drupal_map_assoc(array_keys($matches));
+  drupal_json_output($matches);
+}
+
+/**
+ * Outputs the best token type matches for the given string in JSON format.
+ *
+ * There is no return value (output is sent to the browser). The output is
+ * an array of the closest matches between the input string and existing
+ * tokens, in JSON format.
+ *
+ * @param array $token_type
+ *   The type of token (array of token information) to look for matches on.
+ * @param string ...
+ *   The rest of the arguments are used for matching.
+ */
+function token_autocomplete_token($token_type) {
+  $args = func_get_args();
+  array_shift($args);
+  $string = trim(implode('/', $args));
+  $string = substr($string, strrpos($string, '['));
+
+  $token_type = $token_type['type'];
+  $matches = array();
+
+  if (!drupal_strlen($string)) {
+    $matches["[{$token_type}:"] = 0;
+  }
+  else {
+    $depth = max(1, substr_count($string, ':'));
+    $tree = token_build_tree($token_type, array('flat' => TRUE, 'depth' => $depth));
+    foreach (array_keys($tree) as $token) {
+      if (strpos($token, $string) === 0) {
+        $matches[$token] = levenshtein($token, $string);
+        if (isset($tree[$token]['children'])) {
+          $token = rtrim($token, ':]') . ':';
+          $matches[$token] = levenshtein($token, $string);
+        }
+      }
+    }
+  }
+
+  asort($matches);
+  $matches = drupal_map_assoc(array_keys($matches));
+  drupal_json_output($matches);
+}
diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc
index 263add5..7447e60 100644
--- a/core/modules/user/user.admin.inc
+++ b/core/modules/user/user.admin.inc
@@ -386,7 +386,21 @@ function user_admin_settings($form, &$form_state) {
   );
   // These email tokens are shared for all settings, so just define
   // the list once to help ensure they stay in sync.
-  $email_token_help = t('Available variables are: [site:name], [site:url], [user:name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:cancel-url].');
+  if (module_exists('token')) {
+    $email_token_help = t('The list of available tokens that can be used in e-mail messages is provided below.');
+
+    // @todo Do we need token validation as well?
+
+    $form['token_tree'] = array(
+      '#theme' => 'token_tree',
+      '#token_types' => array('user'),
+      '#show_restricted' => TRUE,
+      '#weight' => 90,
+    );
+  }
+  else {
+    $email_token_help = t('Available variables are: [site:name], [site:url], [user:name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:cancel-url].');
+  }
 
   $form['email_admin_created'] = array(
     '#type' => 'details',
