Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.823
diff -u -r1.823 common.inc
--- includes/common.inc	10 Nov 2008 05:22:59 -0000	1.823
+++ includes/common.inc	11 Nov 2008 04:18:01 -0000
@@ -2715,6 +2715,7 @@
   require_once DRUPAL_ROOT . '/includes/form.inc';
   require_once DRUPAL_ROOT . '/includes/mail.inc';
   require_once DRUPAL_ROOT . '/includes/actions.inc';
+  require_once DRUPAL_ROOT . '/includes/token.inc';
   // Set the Drupal custom error handler.
   set_error_handler('_drupal_error_handler');
   set_exception_handler('_drupal_exception_handler');
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.635
diff -u -r1.635 system.module
--- modules/system/system.module	31 Oct 2008 02:18:22 -0000	1.635
+++ modules/system/system.module	11 Nov 2008 04:18:06 -0000
@@ -2151,3 +2151,34 @@
 function system_image_toolkits() {
   return array('gd');
 }
+
+/**
+ * Implementation of hook_token_values().
+ */
+function system_token_values($type, $object = NULL) {
+  $values = array();
+  switch ($type) {
+    case 'global':
+      global $base_url;
+      $values['site-url']     = $base_url;
+      $values['site-name']    = variable_get('site_name', t('Drupal'));
+      $values['site-slogan']  = variable_get('site_slogan', '');
+      $values['site-mail']    = variable_get('site_mail', '');
+      $values['site-date']    = format_date(time(), 'short', '', variable_get('date_default_timezone', 0));
+      break;
+  }
+  return $values;
+}
+
+/**
+ * Implementation of hook_token_list().
+ */
+function system_token_list($type = 'all') {
+  $tokens = array();
+  $tokens['global']['site-url']     = t('The url of the current Drupal website.');
+  $tokens['global']['site-name']    = t('The name of the current Drupal website.');
+  $tokens['global']['site-slogan']  = t('The slogan of the current Drupal website.');
+  $tokens['global']['site-mail']    = t('The contact email address for the current Drupal website.');
+  $tokens['global']['site-date']    = t("The current date on the site's server.");
+  return $tokens;
+}
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.661
diff -u -r1.661 comment.module
--- modules/comment/comment.module	8 Nov 2008 20:43:54 -0000	1.661
+++ modules/comment/comment.module	11 Nov 2008 04:18:04 -0000
@@ -2020,3 +2020,79 @@
     ),
   );
 }
+
+/**
+ * Implementation of hook_token_values().
+ */
+function comment_token_values($type, $object = NULL) {
+  $values = array();
+  switch ($type) {
+    case 'comment':
+
+      // Cast to an object just in case fussy drupal gave us an array
+      $comment = (object)$object;
+
+      $values['comment-nid']            = $comment->nid;
+      $values['comment-title']          = check_plain($comment->subject);
+      $values['comment-body']           = check_markup($comment->comment, $comment->format);
+      $values['comment-author-name']    = check_plain($comment->name);
+      $values['comment-author-uid']     = $comment->uid;
+
+      // Raw counterparts of user supplied data.
+      $values['comment-title-raw']       = $comment->subject;
+      $values['comment-body-raw']        = $comment->comment;
+      $values['comment-author-name-raw'] = $comment->name;
+
+      // Included in case a consuming module wants to format the body
+      $values['comment-body-format']     = $comment->format;
+
+
+      $values['comment-yyyy']           = date('Y', $comment->timestamp);
+      $values['comment-yy']             = date('y', $comment->timestamp);
+      $values['comment-month']          = date('F', $comment->timestamp);
+      $values['comment-mon']            = date('M', $comment->timestamp);
+      $values['comment-mm']             = date('m', $comment->timestamp);
+      $values['comment-m']              = date('n', $comment->timestamp);
+      $values['comment-ww']             = date('W', $comment->timestamp);
+      $values['comment-date']           = date('N', $comment->timestamp);
+      $values['comment-day']            = date('l', $comment->timestamp);
+      $values['comment-ddd']            = date('D', $comment->timestamp);
+      $values['comment-dd']             = date('d', $comment->timestamp);
+      $values['comment-d']              = date('j', $comment->timestamp);
+      break;
+  }
+
+  return $values;
+}
+
+/**
+ * Implementation of hook_token_list().
+ */
+function comment_token_list($type = 'all') {
+  if ($type == 'comment' || $type == 'all') {
+    $tokens['comment']['comment-nid']             = t('Comment ID');
+    $tokens['comment']['comment-title']           = t('Comment title');
+    $tokens['comment']['comment-title-raw']       = t('Comment title. WARNING - raw user input.');
+    $tokens['comment']['comment-body']            = t('Comment body');
+    $tokens['comment']['comment-body-raw']        = t('Comment body. WARNING - raw user input.');
+
+    $tokens['comment']['comment-author-uid']      = t("Comment author's user id");
+    $tokens['comment']['comment-author-name']     = t("Comment author's user name");
+    $tokens['comment']['comment-author-name-raw'] = t("Comment author's user name. WARNING - raw user input.");
+
+    $tokens['comment']['comment-yyyy']            = t("Comment creation year (four digit)");
+    $tokens['comment']['comment-yy']              = t("Comment creation year (two digit)");
+    $tokens['comment']['comment-month']           = t("Comment creation month (full word)");
+    $tokens['comment']['comment-mon']             = t("Comment creation month (abbreviated)");
+    $tokens['comment']['comment-mm']              = t("Comment creation month (two digit, zero padded)");
+    $tokens['comment']['comment-m']               = t("Comment creation month (one or two digit)");
+    $tokens['comment']['comment-ww']              = t("Comment creation week (two digit)");
+    $tokens['comment']['comment-date']            = t("Comment creation date (day of month)");
+    $tokens['comment']['comment-day']             = t("Comment creation day (full word)");
+    $tokens['comment']['comment-ddd']             = t("Comment creation day (abbreviation)");
+    $tokens['comment']['comment-dd']              = t("Comment creation day (two digit, zero-padded)");
+    $tokens['comment']['comment-d']               = t("Comment creation day (one or two digit)");
+
+    return $tokens;
+  }
+}
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.995
diff -u -r1.995 node.module
--- modules/node/node.module	10 Nov 2008 05:19:46 -0000	1.995
+++ modules/node/node.module	11 Nov 2008 04:18:05 -0000
@@ -2925,3 +2925,129 @@
 
   return $perms;
 }
+
+/**
+ * Implementation of hook_token_values().
+ */
+function node_token_values($type, $object = NULL) {
+  $values = array();
+  switch ($type) {
+    case 'node':
+      $node = $object;
+
+      $values['nid']             = $node->nid;
+      $values['type']            = $node->type;
+      $values['type-name']       = node_get_types('name', $node->type);
+      $values['title']           = check_plain($node->title);
+      $values['title-raw']       = $node->title;
+
+      $values['author-uid']      = $node->uid;
+      $values['author-name']     = check_plain($node->name);
+      $values['author-name-raw'] = $node->name;
+
+      $values['yyyy']           = date('Y', $node->created);
+      $values['yy']             = date('y', $node->created);
+      $values['month']          = date('F', $node->created);
+      $values['mon']            = date('M', $node->created);
+      $values['mm']             = date('m', $node->created);
+      $values['m']              = date('n', $node->created);
+      $values['ww']             = date('W', $node->created);
+      $values['date']           = date('N', $node->created);
+      $values['day']            = date('l', $node->created);
+      $values['ddd']            = date('D', $node->created);
+      $values['dd']             = date('d', $node->created);
+      $values['d']              = date('j', $node->created);
+
+      $values['mod-yyyy']       = date('Y', $node->changed);
+      $values['mod-yy']         = date('y', $node->changed);
+      $values['mod-month']      = date('F', $node->changed);
+      $values['mod-mon']        = date('M', $node->changed);
+      $values['mod-mm']         = date('m', $node->changed);
+      $values['mod-m']          = date('n', $node->changed);
+      $values['mod-ww']         = date('W', $node->changed);
+      $values['mod-date']       = date('N', $node->changed);
+      $values['mod-day']        = date('l', $node->changed);
+      $values['mod-ddd']        = date('D', $node->changed);
+      $values['mod-dd']         = date('d', $node->changed);
+      $values['mod-d']          = date('j', $node->changed);
+
+      // Now get the menu related information.
+      global $_menu;
+      $trail = array();
+      $trail_raw = array();
+      $original_mid = token_menu_get_mid('node/'.$node->nid);
+      $mid = $original_mid;
+      while ($mid && $_menu['visible'][$mid]) {
+        array_unshift($trail, check_plain($_menu['visible'][$mid]['title']));
+        $mid = $_menu['visible'][$mid]['pid'];
+      }
+
+      // One more time, unfiltered
+      $mid = $original_mid;
+      while ($mid && $_menu['visible'][$mid] && $_menu['visible'][$mid]['pid'] != 0) {
+        array_unshift($trail_raw, $_menu['visible'][$mid]['title']);
+        $mid = $_menu['visible'][$mid]['pid'];
+      }
+
+      if (isset($trail)) {
+        $values['menupath']     = implode('/', $trail);
+        $values['menupath-raw'] = implode('/', $trail_raw);
+        $values['menu']         = array_shift($trail);
+      }
+      else {
+        $values['menu']         = '';
+        $values['menupath']     = '';
+        $values['menupath-raw'] = '';
+      }
+
+      if (!isset($values['term'])) {
+        $values['term'] = '';
+        $values['term-raw'] = '';
+        $values['term-id'] = '';
+        $values['vocab'] = '';
+        $values['vocab-raw'] = '';
+        $values['vocab-id'] = '';
+      }
+      break;
+  }
+
+  return $values;
+}
+
+/**
+ * Implementation of hook_token_list().
+ */
+function node_token_list($type = 'all') {
+  if ($type == 'node' || $type == 'all') {
+    $tokens['node']['nid']             = t('Node ID');
+    $tokens['node']['type']            = t('Node type');
+    $tokens['node']['type-name']       = t('Node type (user-friendly version)');
+    $tokens['node']['title']           = t('Node title');
+    $tokens['node']['title-raw']       = t('Unfiltered node title. WARNING - raw user input.');
+
+    $tokens['node']['author-uid']      = t("Node author's user id");
+    $tokens['node']['author-name']     = t("Node author's user name");
+    $tokens['node']['author-name-raw'] = t("Node author's user name. WARNING - raw user input.");
+
+
+    $tokens['node']['yyyy']            = t("Node creation year (four digit)");
+    $tokens['node']['yy']              = t("Node creation year (two digit)");
+    $tokens['node']['month']           = t("Node creation month (full word)");
+    $tokens['node']['mon']             = t("Node creation month (abbreviated)");
+    $tokens['node']['mm']              = t("Node creation month (two digit, zero padded)");
+    $tokens['node']['m']               = t("Node creation month (one or two digit)");
+    $tokens['node']['ww']              = t("Node creation week (two digit)");
+    $tokens['node']['date']            = t("Node creation date (day of month)");
+    $tokens['node']['day']             = t("Node creation day (full word)");
+    $tokens['node']['ddd']             = t("Node creation day (abbreviation)");
+    $tokens['node']['dd']              = t("Node creation day (two digit, zero-padded)");
+    $tokens['node']['d']               = t("Node creation day (one or two digit)");
+    $tokens['node']['mod-????']        = t('All tokens for node creation dates can also be used with with the "mod-" prefix; doing so will use the modification date rather than the creation date.');
+
+    $tokens['node']['menu']            = t("The name of the menu the node belongs to.");
+    $tokens['node']['menupath']        = t("The menu path (as reflected in the breadcrumb), not including Home or [menu]. Separated by /.");
+    $tokens['node']['menupath-raw']    = t("The unfiltered menu path (as reflected in the breadcrumb), not including Home or [menu]. Separated by /. WARNING - raw user input.");
+
+    return $tokens;
+  }
+}
Index: modules/taxonomy/taxonomy.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v
retrieving revision 1.440
diff -u -r1.440 taxonomy.module
--- modules/taxonomy/taxonomy.module	9 Nov 2008 00:58:03 -0000	1.440
+++ modules/taxonomy/taxonomy.module	11 Nov 2008 04:18:09 -0000
@@ -1402,3 +1402,83 @@
     ),
   );
 }
+
+/**
+ * Implementation of hook_token_values().
+ */
+function taxonomy_token_values($type, $object = NULL) {
+  $values = array();
+  switch ($type) {
+    case 'taxonomy':
+      $category = $object;
+
+      $vid = $category->vid;
+      $vocabulary = taxonomy_vocabulary_load($vid);
+      $values['vid']       = $vid;
+      $values['vocab']     = check_plain($vocabulary->name);
+      $values['cat']       = check_plain($category->name);
+      $values['tid']       = $category->tid;
+      $values['vocab-raw'] = $vocabulary->name;
+      $values['cat-raw']   = $category->name;
+
+      break;
+    case 'node':
+      $node = $object;
+      // And now taxonomy, which is a bit more work. This code is adapted from
+      // pathauto's handling code; it's intended for compatability with it.
+      if (!empty($node->taxonomy) && is_array($node->taxonomy)) {
+        foreach ($node->taxonomy as $term) {
+          if ((object)$term) {
+            // With freetagging it's somewhat hard to get the tid, vid, name values
+            // Rather than duplicating taxonomy.module code here you should make sure your calling module
+            // has a weight of at least 1 which will run after taxonomy has saved the data which allows us to
+            // pull it out of the db here.
+            if (!isset($term->name) || !isset($term->tid)) {
+              $vid = db_result(db_query("SELECT t.vid FROM {term_node} r INNER JOIN {term_data} t ON r.tid = t.tid INNER JOIN {vocabulary} v ON t.vid = v.vid WHERE r.nid = %d ORDER BY v.weight, t.weight, t.name LIMIT 1", $object->nid));
+              $term = db_fetch_object(db_query("SELECT t.tid, t.name FROM {term_data} t INNER JOIN {term_node} r ON r.tid = t.tid WHERE t.vid = %d AND r.nid = %d ORDER BY weight LIMIT 1", $vid, $object->nid));
+              $term->vid = $vid;
+            }
+
+            $values['term'] = check_plain($term->name);
+            $values['term-raw'] = $term->name;
+            $values['term-id'] = $term->tid;
+            $vid = $term->vid;
+
+            if (!empty($vid)) {
+              $vocabulary = taxonomy_vocabulary_load($vid);
+              $values['vocab'] = check_plain($vocabulary->name);
+              $values['vocab-raw'] = $vocabulary->name;
+              $values['vocab-id'] = $vocabulary->name;
+            }
+
+            break;
+          }
+        }
+      }
+    break;
+  }
+  return $values;
+}
+
+/**
+ * Implementation of hook_token_list().
+ */
+function taxonomy_token_list($type = 'all') {
+  if ($type == 'taxonomy' || $type == 'all') {
+    $tokens['taxonomy']['tid']       = t("The id number of the category's parent vocabulary.");
+    $tokens['taxonomy']['vocab']     = t("The vocabulary that the page's first category belongs to.");
+    $tokens['taxonomy']['cat']       = t('The name of the category.');
+    $tokens['taxonomy']['tid']       = t('The id number of the category.');
+    $tokens['taxonomy']['vocab-raw'] = t("The unfiltered vocabulary that the page's first category belongs to. WARNING - raw user input.");
+    $tokens['taxonomy']['cat-raw']   = t('The unfiltered name of the category. WARNING - raw user input.');
+    if ($type == 'node' || $type == 'all') {
+      $tokens['node']['term']            = t("Name of top taxonomy term");
+      $tokens['node']['term-raw']        = t("Unfiltered name of top taxonomy term. WARNING - raw user input.");
+      $tokens['node']['term-id']         = t("ID of top taxonomy term");
+      $tokens['node']['vocab']           = t("Name of top term's vocabulary");
+      $tokens['node']['vocab-raw']       = t("Unfiltered name of top term's vocabulary. WARNING - raw user input.");
+      $tokens['node']['vocab-id']        = t("ID of top term's vocabulary");
+    }
+    return $tokens;
+  }
+}
Index: modules/book/book.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.module,v
retrieving revision 1.473
diff -u -r1.473 book.module
--- modules/book/book.module	29 Oct 2008 10:08:51 -0000	1.473
+++ modules/book/book.module	11 Nov 2008 04:18:02 -0000
@@ -1169,3 +1169,57 @@
 
   return $tree[$cid];
 }
+
+/**
+ * Implementation of hook_token_values().
+ */
+function book_token_values($type, $object = NULL) {
+  if ($type == 'node') {
+    $node = $object;
+
+    $tokens = array();
+    if (isset($node->parent)) {
+      $path = book_location($node);
+      $tokens['book']     = check_plain($path[0]->title);
+      $tokens['book-raw'] = $path[0]->title;
+      $tokens['book_id']  = $node->parent;
+
+      $bookhierarchy = book_location($node);
+      $bookpath = '';
+      $bookpath_raw = '';
+      foreach ($bookhierarchy as $bookelement) {
+        if ($bookpath == '') {
+          $bookpath = check_plain($bookelement->title);
+          $bookpath_raw = $bookelement->title;
+        }
+        else {
+          $bookpath = $bookpath .'/'. check_plain($bookelement->title);
+          $bookpath_raw = $bookpath_raw . '/' . $bookelement->title;
+        }
+      }
+      $tokens['bookpath'] = $bookpath;
+      $tokens['bookpath-raw'] = $bookpath_raw;
+    }
+    else {
+      $tokens['book'] = '';
+      $tokens['book_id'] = '';
+      $tokens['bookpath'] = '';
+      $tokens['bookpath-raw'] = '';
+    }
+    return $tokens;
+  }
+}
+
+/**
+ * Implementation of hook_token_list()
+ */
+function book_token_list($type) {
+  if ($type == 'node' || $type == 'all') {
+    $list['book']['book'] = t("The title of the node's book parent.");
+    $list['book']['book-raw'] = t("The unfiltered title of the node's book parent. WARNING - raw user input.");
+    $list['book']['book_id'] = t("The id of the node's book parent.");
+    $list['book']['bookpath'] = t("The titles of all parents in the node's book hierarchy.");
+    $list['book']['bookpath-raw'] = t("The unfiltered titles of all parents in the node's book hierarchy. WARNING - raw user input.");
+    return $list;
+  }
+}
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.932
diff -u -r1.932 user.module
--- modules/user/user.module	10 Nov 2008 05:23:01 -0000	1.932
+++ modules/user/user.module	11 Nov 2008 04:18:09 -0000
@@ -2447,3 +2447,67 @@
   return empty($groups) ? FALSE : $groups;
 }
 
+/**
+ * Implementation of hook_token_values()
+ */
+function user_token_values($type, $object = NULL) {
+  $values = array();
+  switch ($type) {
+    case 'user':
+      if (isset($object)) {
+        $account = $object;
+      }
+      else {
+        global $user;
+        $account = user_load(array('uid' => $user->uid));
+      }
+
+      $values['user']           = $account->uid ? check_plain($account->name) : variable_get('anonymous', 'Anonymous');
+      $values['user-raw']       = $account->uid ? $account->name : variable_get('anonymous', 'Anonymous');
+      $values['uid']            = $account->uid;
+      $values['mail']           = $account->uid ? $account->mail : '';
+      $values['reg-date']       = $account->uid ? format_date($account->created, 'short') : '';
+      $values['reg-since']      = $account->uid ? format_interval($account->created) : '';
+      $values['log-date']       = $account->uid ? format_date($account->access, 'short') : '';
+      $values['log-since']      = $account->uid ? format_interval($account->access) : '';
+      $values['date-in-tz']     = $account->uid ? format_date(time(), 'short', '', $account->timezone) : '';
+      $values['account-url']    = $account->uid ? url("user/$account->uid") : '';
+      $values['account-edit']   = $account->uid ? url("user/$account->uid/edit") : '';
+
+      break;
+    case 'global':
+      global $user;
+      $values['user-name']    = $user->uid ? $user->name : variable_get('anonymous', t('Anonymous'));
+      $values['user-id']      = $user->uid ? $user->uid : 0;
+      $values['user-mail']    = $user->uid ? $user->mail : '';
+      break;
+  }
+  return $values;
+}
+
+/**
+ * Implementation of hook_token_list()
+ */
+function user_token_list($type = 'all') {
+  $tokens = array();
+  if ($type == 'user' || $type == 'all') {
+    $tokens['user']['user']           = t("User's name");
+    $tokens['user']['user-raw']       = t("User's unfiltered name. WARNING - raw user input.");
+    $tokens['user']['uid']            = t("User's ID");
+    $tokens['user']['mail']           = t("User's email address");
+
+    $tokens['user']['reg-date']       = t("User's registration date");
+    $tokens['user']['reg-since']      = t("Days since the user registered");
+    $tokens['user']['log-date']       = t("User's last login date");
+    $tokens['user']['log-since']      = t("Days since the user's last login");
+    $tokens['user']['date-in-tz']     = t("The current date in the user's timezone");
+    $tokens['user']['account-url']    = t("The URL of the user's profile page.");
+    $tokens['user']['account-edit']   = t("The URL the user's account editing page.");
+  }
+
+  $tokens['global']['user-name']    = t('The name of the currently logged in user.');
+  $tokens['global']['user-id']      = t('The user ID of the currently logged in user.');
+  $tokens['global']['user-mail']    = t('The email address of the currently logged in user.');
+  
+  return $tokens;
+}
Index: includes/token.inc
===================================================================
RCS file: includes/token.inc
diff -N includes/token.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/token.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,272 @@
+<?php
+// $Id$
+
+/**
+ * Return the value of $original, with all instances of placeholder
+ * tokens replaced by their proper values. To replace mutliple types
+ * at once see token_replace_multiple().
+ *
+ * @param $original
+ *   A string, or an array of strings, to perform token substitutions
+ *   on.
+ * @param $type
+ *   A flag indicating the class of substitution tokens to use. If an
+ *   object is passed in the second param, 'type' should contain the
+ *   object's type. For example, 'node', 'comment', or 'user'. If no
+ *   type is specified, only 'global' site-wide substitution tokens are
+ *   built.
+ * @param $object
+ *   Optionally, the object to use for building substitution values.
+ *   A node, comment, user, etc.
+ * @param $leading
+ *   Character(s) to prepend to the token key before searching for
+ *   matches. Defaults to an open-bracket.
+ * @param $trailing
+ *   Character(s) to append to the token key before searching for
+ *   matches. Defaults to a close-bracket.
+ * @return
+ *   The modified version of $original, with all substitutions made.
+ */
+function token_replace($original, $type = 'global', $object = NULL, $leading = '[', $trailing = ']') {
+  $full = token_get_values($type, $object);
+  return _token_replace_tokens($original, $full->tokens, $full->values, $leading, $trailing);
+}
+
+/**
+ * Return the value of $original, with all instances of placeholder
+ * tokens replaced by their proper values. Contrary to token_replace(),
+ * this function supports replacing mutiple types.
+ *
+ * @param $original
+ *   A string, or an array of strings, to perform token substitutions
+ *   on.
+ * @param $types
+ *   An array of substitution classes and optional objects. The key is
+ *   a flag indicating the class of substitution tokens to use.
+ *   If an object is passed as value, the key should contain the
+ *   object's type. For example, 'node', 'comment', or 'user'. The
+ *   object will be used for building substitution values. If no type
+ *   is specified, only 'global' site-wide substitution tokens are built.
+ * @param $leading
+ *   Character(s) to prepend to the token key before searching for
+ *   matches. Defaults to an open-bracket.
+ * @param $trailing
+ *   Character(s) to append to the token key before searching for
+ *   matches. Defaults to a close-bracket.
+ * @return The modified version of $original, with all substitutions
+ *   made.
+ */
+function token_replace_multiple($original, $types = array('global' => NULL), $leading = '[', $trailing = ']') {
+  $full = new stdClass();
+  $full->tokens = $full->values = array();
+  foreach ($types as $type => $object) {
+    $temp = token_get_values($type, $object);
+    $full->tokens = array_merge($full->tokens, $temp->tokens);
+    $full->values = array_merge($full->values, $temp->values);
+  }
+  return _token_replace_tokens($original, $full->tokens, $full->values, $leading, $trailing);
+}
+
+// Internally used function to replace tokens.
+function _token_replace_tokens($original, $tokens, $values, $leading, $trailing) {
+  $tokens = token_prepare_tokens($tokens, $leading, $trailing);
+  return str_replace($tokens, $values, $original);
+}
+
+/**
+ * Return a list of valid substitution tokens and their values for
+ * the specified type.
+ *
+ * @param $type
+ *   A flag indicating the class of substitution tokens to use. If an
+ *   object is passed in the second param, 'type' should contain the
+ *   object's type. For example, 'node', 'comment', or 'user'. If no
+ *   type is specified, only 'global' site-wide substitution tokens are
+ *   built.
+ * @param $object
+ *   Optionally, the object to use for building substitution values.
+ *   A node, comment, user, etc.
+ * @param $flush
+ *   Optional.  If TRUE, the internal token cache is cleared.
+ * @param $test_context
+ *   Optional (only used for testing purposes).  If provided, the internally
+ *   cached tokens and recursion depth are replaced with those from the
+ *   test_context.
+ * @return
+ *   A keyed array containing the substitution tokens and the substition
+ *   values for the passed-in type and object.
+ */
+function token_get_values($type = 'global', $object = NULL, $flush = FALSE, $test_context = NULL) {
+  static $tokens;
+  static $depth;
+  
+  // If this function is being called during a unit test, set up the static variables
+  // according to the needs of the test.  This would be cleaner if implemented as
+  // a class.
+  if (isset($test_context)) {
+    $tokens = $test_context->tokens;
+    $depth = $test_context->depth;
+  }
+
+  // Flush the static token cache. Useful for processes that need to slog through
+  // huge numbers of tokens in a single execution cycle. Flushing it will keep
+  // them from burning through memory.
+  if ($flush || !isset($tokens)) {
+    $tokens = array();
+  }
+
+  // Simple recursion check. This is to avoid content_view()'s potential
+  // for endless looping when a filter uses tokens, which load the content
+  // view, which calls the filter, which uses tokens, which...
+  if (!isset($depth)) {
+    $depth = 1;
+  }
+  elseif ($depth = 1) {
+    $depth++;
+  }
+  else {
+    // We'll allow things to get two levels deep, but bail out after that
+    // without performing any substitutions.
+    $result = new stdClass();
+    $result->tokens = array();
+    $result->values = array();
+    return $result;
+  }
+
+  $id = _token_get_id($type, $object);
+  if (isset($tokens[$type][$id])) {
+    $tmp_tokens = $tokens[$type][$id];
+  }
+  else {
+    $tmp_tokens = module_invoke_all('token_values', $type, $object);
+    $tokens[$type][$id] = $tmp_tokens;
+  }
+
+  // Special-case global tokens, as we always want to be able to process
+  // those substitutions.
+  if (!isset($tokens['global']['default'])) {
+    $tokens['global']['default'] = module_invoke_all('token_values', 'global');
+  }
+
+  $all = array_merge($tokens['global']['default'], $tokens[$type][$id]);
+
+  $result = new stdClass();
+  $result->tokens = array_keys($all);
+  $result->values = array_values($all);
+
+  $depth--;
+
+  return $result;
+}
+
+/**
+ * A helper function that retrieves all currently exposed tokens,
+ * and merges them recursively. This is only necessary when building
+ * the token listing -- during actual value replacement, only tokens
+ * in a particular domain are requested and a normal array_marge() is
+ * sufficient.
+ *
+ * @param $type
+ *   A flag indicating the class of substitution tokens to use. If an
+ *   object is passed in the second param, 'type' should contain the
+ *   object's type. For example, 'node', 'comment', or 'user'. If no
+ *   type is specified, only 'global' site-wide substitution tokens are
+ *   built.
+ * @return
+ *   The array of usable tokens and their descriptions, organized by
+ *   token type.
+ */
+function token_get_list($type = 'all') {
+  $return = array();
+  foreach (module_implements('token_list') as $module) {
+    $function = $module .'_token_list';
+    $result = $function($type);
+    if (is_array($result)) {
+      foreach ($result as $category => $tokens) {
+        foreach ($tokens as $token => $title) {
+          $return[$category][$token] = $title;
+        }
+      }
+    }
+  }
+  return $return;
+}
+
+/**
+ * A helper function that transforms all the elements of an
+ * array. Used to change the delimiter style from brackets to
+ * percent symbols etc.
+ *
+ * @param $tokens
+ *   The array of tokens keys with no delimiting chacaters.
+ * @param $leading
+ *   Character(s) to prepend to the token key before searching for
+ *   matches. Defaults to an open-bracket.
+ * @param $trailing
+ *   Character(s) to append to the token key before searching for
+ *   matches. Defaults to a close-bracket.
+ * @return
+ *   The array of token keys, each wrapped in the specified
+ *   delimiter style.
+ */
+function token_prepare_tokens($tokens = array(), $leading = '[', $trailing = ']') {
+  if (!isset($tokens)) {
+    $tokens = array();
+  }
+  foreach ($tokens as $key => $value) {
+    $tokens[$key] = $leading . $value . $trailing;
+  }
+  return $tokens;
+}
+
+/**
+ * Internal utility function used for static caching. There are
+ * almost certainly better ways to do this, but for the moment it's
+ * sufficient.
+ */
+function _token_get_id($type = 'global', $object = NULL) {
+  if (!isset($object)) {
+    return 'default';
+  }
+  switch ($type) {
+    case 'node':
+      return $object->nid;
+    case 'comment':
+      return $object->cid;
+    case 'user':
+      return $object->uid;
+    default:
+      return crc32(serialize($object));
+  }
+}
+
+/**
+ * Check if mid/path is present in the menu.
+ *
+ * @param $in
+ *   Numeric input is treated as a menu-id, strings as src-paths.
+ * @return
+ *   An existing mid, or 0 if none found.
+ */
+function token_menu_get_mid($in) {
+  global $_menu;
+
+  if (!is_numeric($in)) {
+    if (isset($_menu['path index'][$in])) {
+      $mid = $_menu['path index'][$in];
+    }
+    else {
+      $mid = 0;
+    }
+  }
+  else if (!isset($_menu['visible'][$in])) {
+    $mid = 0;
+  }
+
+  // Temporary paths would break much of this module.
+  if ($mid < 0) {
+    $mid = 0;
+  }
+
+  return $mid;
+}
Index: modules/simpletest/tests/token.test
===================================================================
RCS file: modules/simpletest/tests/token.test
diff -N modules/simpletest/tests/token.test
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/token.test	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,248 @@
+<?php
+
+/**
+ * Test the Token API.
+ */
+class TokenTest extends DrupalWebTestCase {
+  
+  /**
+   * Implementation of getInfo().
+   */
+  function getInfo() {
+    return array(
+      'name' => t('Token API'),
+      'description' => t('Tests for the token API'),
+      'group' => t('Token')
+    );
+  }
+  
+  /**
+   * Verifies the results of token_prepare_tokens.  This function takes an
+   * array of tokens and creates a copy in which the values for the tokens
+   * have a prefix and suffix.
+   *
+   * @param array $orig_tokens The original token array passed into token_prepare_tokens.
+   * @param array $modified_tokens The array resulting from a call to token_prepare_tokens.
+   * @param string $leading The string representing the value prefix.
+   * @param string $trailing The string representing the value suffix.
+   * @return object that indicates whether the modified tokens array is correct
+   * based on the original tokens and leading and trailing strings.
+   */
+  function verifyPreparedTokens($orig_tokens = array(), $modified_tokens, $leading = '[', $trailing = ']') {
+    if (!isset($orig_tokens)) {
+      $orig_tokens = array();
+    }
+    $result = new stdClass();
+    $result->verified = FALSE;
+    if (sizeof($orig_tokens) != sizeof($modified_tokens)) {
+      $result->message = 'Size of the arrays do not match.';
+      return $result;
+    }
+    foreach ($orig_tokens as $key => $value) {
+      $new_value = $leading . $value . $trailing;
+      if ($modified_tokens[$key] != $new_value) {
+        $result->message = 'New value for ' . $value . ' should be ' .$new_value
+          . ' but actually was ' .$modified_tokens[$key];
+        return $result;
+      }
+    }
+    $result->verified = TRUE;
+    $result->message = 'Resulting array is correct.';
+    return $result;
+  }
+  
+  /**
+   * Tests that a simple set of tokens is "prepared" properly by the
+   * token_prepare_tokens function.
+   */
+  function testTokenPrepareTokens() {
+    $tokens = array('user' => 'tester', 'password' => 'test123', 'hobby' => 'welding');
+    $modifiedTokens = token_prepare_tokens($tokens);
+    $result = $this->verifyPreparedTokens($tokens, $modifiedTokens);
+    $this->assertTrue($result->verified, $result->message);
+  }
+
+  /**
+   * Tests the token_prepare_tokens function when passed different leading and
+   * trailing strings.
+   */
+  function testTokenPrepareTokensLeadingAndTrailing() {
+    $tokens = array('user' => 'tester', 'password' => 'test123', 'hobby' => 'welding');
+    $leading = '%';
+    $trailing = '>';
+    $modifiedTokens = token_prepare_tokens($tokens, $leading, $trailing);
+    $result = $this->verifyPreparedTokens($tokens, $modifiedTokens, $leading, $trailing);
+    $this->assertTrue($result->verified, $result->message);
+  }
+
+  /**
+   * Tests the token_prepare_tokens function when passed NULL for the array.
+   */
+  function testTokenPrepareTokensNullTokenList() {
+    $tokens = NULL;
+    $modifiedTokens = token_prepare_tokens($tokens);
+    $result = $this->verifyPreparedTokens($tokens, $modifiedTokens);
+    $this->assertTrue($result->verified, $result->message);
+  }
+  
+  /**
+   * Returns a set of tokens that will be used as the default tokens for all
+   * tests.
+   *
+   * @return An array of tokens (key/value pairs).
+   */
+  function getDefaultTokens() {
+    $tokens = array('site-url' => 'http://localhost', 'site-name' => 'Drupal',
+      'site-slogan' => '', 'site-email' => '',
+      'site-date' => 'Tue, 11/04/2008 - 20:29', 'user-name' => 'admin',
+      'user-id' => '1', 'user-mail' => 'admin@example.com');
+    return $tokens;
+  }
+  
+  /**
+   * Verifies that the tokens returned from token_get_values include
+   * all of the specified expected tokens.  For example, this can be
+   * used to verify that all of the global/default tokens are included. 
+   *
+   * @param obj $tokens the tokens The object returned from token_get_values
+   * @param array $expected_tokens Contains the expected tokens (key/value pair). 
+   * @return An object that contains the result of the check (->verified =
+   * TRUE if successful; FALSE otherwise) and an associated message (->message)
+   * that can be passed to $this->assertTrue().
+   */
+  function checkTokensAreIncluded($tokens, $expected_tokens = NULL) {
+    $result = new stdClass();
+    $result->verified = FALSE;
+    if (!isset($expected_tokens)) {
+      $expected_tokens = $this->getDefaultTokens();
+    }
+    $size = sizeof($tokens->tokens);
+    for ($i = 0; $i < $size; $i++) {
+      $key = $tokens->tokens[$i];
+      $value = $tokens->values[$i];
+      if (!isset($expected_tokens[$key])) {
+        // This does not represent an expected token.
+        continue;
+      }
+      if ($expected_tokens[$key] != $value) {
+        $result->message = 'Value for token ' .$key .' should be ' .$value .
+          ', not ' .$expected_tokens[$key];
+        return $result;
+      }
+      // Remove the matching item from the expected tokens array so a final check
+      // for completeness can efficiently be performed.
+      unset($expected_tokens[$key]);
+    }
+    // Final check that all of the expected tokens were included.
+    if (sizeof($expected_tokens) > 0) {
+      $result->message = 'Some expected tokens were missing: ['
+        .implode(',', array_keys($expected_tokens)) .']';
+      return $result;
+    }
+    $result->verified = TRUE;
+    $result->message = 'Resulting tokens contain the expected set.';
+    return $result;
+  }
+
+  /**
+   * Creates a test context that holds tokens that can be used to verify
+   * the API is working correctly.
+   * 
+   * @return the test context that contains the global/default tokens.
+   */
+  function getTestContext() {
+    $default_tokens = $this->getDefaultTokens();
+    $testContext = new stdClass();
+    $testContext->tokens = array('global' => array('default' => $default_tokens));
+    $testContext->depth = 0;
+    return $testContext; 
+  }
+
+  /**
+   * Adds tokens to the test context.
+   */
+  function addTokens(&$test_context, $type, $id, $new_tokens) {
+    if (!isset($test_context->tokens[$type])) {
+      $test_context->tokens[$type] = array();
+    }
+    if (!isset($test_context->tokens[$type][$id])) {
+      $test_context->tokens[$type][$id] = array();
+    }
+    $test_context->tokens[$type][$id] = array_merge($test_context->tokens[$type][$id], $new_tokens);
+  }
+  
+  /**
+   * Verify that the global, default tokens are returned from token_get_values.
+   */
+  function testTokenGetValues() {
+    $test_context = $this->getTestContext();
+    $this->addTokens($test_context, 'test', 'help',
+      array('help_text' => 'Here is some help text', 'help_status' => 'Success'));
+    $tokens = token_get_values('global', NULL, FALSE, $test_context);
+    $result = $this->checkTokensAreIncluded($tokens);
+    $this->assertTrue($result->verified, $result->message);
+  }
+
+  /**
+   * Verify that the global tokens and the requested tokens are returned
+   * from token_get_values.
+   */
+  function testTokenGetValuesNonglobalType() {
+    $nondefault_tokens = array('test_text' => 'Here is some test text',
+      'status' => 'Success');
+    $test_context = $this->getTestContext();
+    $this->addTokens($test_context, 'test', 'default', $nondefault_tokens);
+    $tokens = token_get_values('test', NULL, FALSE, $test_context);
+    $result = $this->checkTokensAreIncluded($tokens);
+    $this->assertTrue($result->verified, $result->message);
+
+    // Be sure the non-default tokens are also present
+    $result = $this->checkTokensAreIncluded($tokens, $nondefault_tokens);
+    $this->assertTrue($result->verified, $result->message);
+  }
+
+  /**
+   * Make sure the token_replace function is able to replace tokens in a string
+   * from one type ('global' + 1 other).
+   */
+  function testTokenReplace() {
+    $default_tokens = $this->getDefaultTokens();
+    $test_tokens = array('test_text' => 'Here is some test text', 'status' => 'Success');
+    $sample = '[status], [test_text], [site-url]';
+    $test_context = $this->getTestContext();
+    $this->addTokens($test_context, 'test', 'default', $test_tokens);
+    
+    // Need to initialize the cached tokens:
+    $tokens = token_get_values('test', NULL, FALSE, $test_context);
+    
+    // Calculate the expected result
+    $expected_result = implode(', ', array($test_tokens['status'],
+      $test_tokens['test_text'], $default_tokens['site-url']));
+    $result = token_replace($sample, 'test');
+    $this->assertTrue($result == $expected_result, 'Expected result: '
+      .$expected_result .', actual: ' .$result);
+  }
+
+  /**
+   * Make sure the token_replace_multiple function is able to replace tokens
+   * of multiple types ('global' + at least 2 others).
+   */
+  function testTokenReplaceMultiple() {
+    $default_tokens = $this->getDefaultTokens();
+    $test_tokens = array('test_text' => 'Here is some test text', 'status' => 'Success');
+    $other_tokens = array('other_text' => 'Other text here', 'other_mode' => 'Blinking');
+    $sample = '[other_mode], [status], [test_text], [other_text], [site-url]';
+    $test_context = $this->getTestContext();
+    $this->addTokens($test_context, 'test', 'default', $test_tokens);
+    $this->addTokens($test_context, 'other', 'default', $other_tokens);
+    
+    // Need to initialize the cached tokens:
+    $tokens = token_get_values('test', NULL, FALSE, $test_context);
+    
+    // Calculate the expected result
+    $expected_result = implode(', ', array($other_tokens['other_mode'], $test_tokens['status'],
+      $test_tokens['test_text'], $other_tokens['other_text'], $default_tokens['site-url']));
+    $result = token_replace_multiple($sample, array('test' => NULL, 'other' => NULL, 'global' => NULL));
+    $this->assertTrue($result == $expected_result, 'Expected result: ' .$expected_result .', actual: ' .$result);
+  }
+}
