Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.932
diff -u -p -r1.932 common.inc
--- includes/common.inc	10 Jul 2009 05:45:56 -0000	1.932
+++ includes/common.inc	12 Jul 2009 19:55:28 -0000
@@ -4794,3 +4794,162 @@ function _drupal_flush_css_js() {
   }
   variable_set('css_js_query_string', $new_character . substr($string_history, 0, 19));
 }
+
+/**
+ * Replace all tokens in a given string with appropriate values.
+ *
+ * @param $text
+ *   A string potentially containing replacable tokens.
+ * @param $data
+ *   An array of keyed objects. For simple replacement scenarios, 'node', 'user',
+ *   and others are common keys, with an accompanying node or user object being
+ *   the value. Some token types, like 'site', do not require any explicit
+ *   information from $data and can be replaced even if $data is empty.
+ * @param $language
+ *   An optional language object to be used when generating locale-sensitive
+ *   tokens like dates.
+ * * @return
+ *   Text with tokens replaced.
+ */
+function token_replace($text, array $data = array(), $language = NULL) {
+  $token_list = token_scan($text);
+  $replacements = token_generate($token_list, $data, $language, TRUE);
+
+  $tokens = array_keys($replacements);
+  $values = array_values($replacements);
+
+  return str_replace($tokens, $values, $text);
+}
+
+/**
+ * Build a list of all token-like patterns that appear in the text.
+ *
+ * @param $text
+ *   The text to be scanned for possible tokens.
+ * * @return
+ *   An associative array of discovered tokens, grouped by type.
+ */
+function token_scan($text) {
+  // Matches tokens with the following pattern: [$type:$token]
+  // $type and $token may not contain white spaces.
+  preg_match_all('/\[([^\s\]:]*):([^\s\]]*)\]/', $text, $matches);
+
+  $types = $matches[1];
+  $tokens = $matches[2];
+
+  // Iterate through the matches, building an associative array containing
+  // $tokens grouped by $types, pointing to the original "raw" version of the
+  // token found in the source text.
+  // For example, $results['node']['title'] = '[node:title]';
+  $results = array();
+  for ($i = 0; $i < count($tokens); $i++) {
+    $results[$types[$i]][$tokens[$i]] = $matches[0][$i];
+  }
+  return $results;
+}
+
+/**
+ * Generate replacement values for a list of tokens.
+ *
+ * @param $raw_tokens
+ *   A keyed array of tokens, and their original raw form in the source text.
+ * @param $data
+ *   An array of keyed objects. For simple replacement scenarios 'node', 'user',
+ *   and others are common keys, with an accompanying node or user object being
+ *   the value. Some token types, like 'site', do not require any explicit
+ *   information from $data and can be replaced even if $data is empty.
+ * @param $language
+ *   An optional language object to be used when generating locale-sensitive
+ *   tokens like dates.
+ * @param $sanitize
+ *   A boolean flag indicating whether the replacement values should be sanitized
+ *   for display for HTML. Defaults to TRUE. If FALSE, the calling module assumes
+ *   responsibility for sanitizing any returned token values manually.
+ * * @return
+ *   An associative array of replacement values, keyed by the original 'raw'
+ *   tokens that were found in the source text. For example:
+ *   $results['[node:title]'] = 'My new node';
+ */
+function token_generate(array $raw_tokens, array $data = array(), $language = NULL, $sanitize = TRUE) {
+  $results = array();
+  foreach ($raw_tokens as $type => $tokens) {
+    foreach (module_invoke_all('tokens', $type, $tokens, $data, $language, $sanitize) as $original => $replacement) {
+      $results[$original] = $replacement;
+    }
+  }
+  return $results;
+}
+
+/**
+ * Given a list of tokens, return those that begin with a specific prefix.
+ *
+ * Used to extract a group of 'chained' tokens (such as [node:author:name]) from
+ * the full list of tokens found in text. The following code:
+ * 
+ *   $data = array(
+ *     'author:name' => '[node:author:name]',
+ *     'title'       => '[node:title]',
+ *     'created'     => '[node:author:name]',
+ *   );
+ *   token_find_with_prefix($data, 'author');
+ *
+ * would result in the following return value:
+ *
+ *   array('name' => '[node:author:name]');
+ *
+ * @param $tokens
+ *   A keyed array of tokens, and their original raw form in the source text.
+ * @param $prefix
+ *   A textual string to be matched at the beginning of the token.
+ * @param $delimiter
+ *   An optional string containing the character that separates the prefix from
+ *   the rest of the token. Defaults to ':'.
+ * @return
+ *   An associative array of discovered tokens, with the prefix and delimiter
+ *   stripped from the key.
+ */
+function token_find_with_prefix(array $tokens, $prefix, $delimiter = ':') {
+  $results = array();
+  foreach ($tokens as $token => $raw) {
+    $parts = split($delimiter, $token, 2);
+    if (count($parts) == 2 && $parts[0] == $prefix) {
+      $results[$parts[1]] = $raw;
+    }
+  }
+  return $results;
+}
+
+/**
+ * Returns metadata describing supported tokens.
+ *
+ * The metadata array contains token type, name, and description data as well as
+ * an optional pointer indicating that the token chains to another set of tokens.
+ * For example:
+ *
+ *   $data['types']['node'] = array(
+ *     'name' => t('Nodes'),
+ *     'description' => t('Tokens related to node objects.'),
+ *   );
+ *   $data['tokens']['node']['title'] = array(
+ *     'name' => t('Title'),
+ *     'description' => t('The title of the current node.'),
+ *   );
+ *   $data['tokens']['node']['author'] = array(
+ *     'name' => t('Author'),
+ *     'description' => t('The author of the current node.'),
+ *     'references' => 'user',
+ *   );
+ *
+ * @param $reset
+ *   Rebuild the internal cache of Token metadata. Defaults to FALSE.
+ * * @return
+ *   An associative array of token information, grouped by token type.
+ */
+function token_info($reset = FALSE) {
+  $data = &drupal_static(__FUNCTION__);
+  if (!isset($data)) {
+    $data = module_invoke_all('token_info');
+    drupal_alter('token_info', $data);
+  }
+  return $data;
+}
Index: modules/comment/comment.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.info,v
retrieving revision 1.10
diff -u -p -r1.10 comment.info
--- modules/comment/comment.info	8 Jun 2009 09:23:51 -0000	1.10
+++ modules/comment/comment.info	12 Jul 2009 19:55:28 -0000
@@ -10,3 +10,4 @@ files[] = comment.admin.inc
 files[] = comment.pages.inc
 files[] = comment.install
 files[] = comment.test
+files[] = comment.tokens.inc
Index: modules/comment/comment.tokens.inc
===================================================================
RCS file: modules/comment/comment.tokens.inc
diff -N modules/comment/comment.tokens.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/comment/comment.tokens.inc	12 Jul 2009 19:55:28 -0000
@@ -0,0 +1,239 @@
+<?php
+// $Id: comment.tokens.inc,v 1.12 2009/07/12 16:27:54 eaton Exp $
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for comment-related data.
+ */
+
+/**
+ * Implement hook_tokens().
+ */
+function comment_tokens($type, $tokens, $data, $language = NULL, $sanitize = TRUE) {
+  $url_options = array('absolute' => TRUE);
+  if (isset($language)) {
+    $url_options['language'] = $language;
+  }
+
+  $replacements = array();
+
+  if ($type == 'comment' && !empty($data['comment'])) {
+    $comment = $data['comment'];
+
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        // Simple key values on the comment.
+        case 'cid':
+          $replacements[$original] = $comment->cid;
+          break;
+
+        case 'nid':
+          $replacements[$original] = $comment->nid;
+          break;
+
+        case 'uid':
+          $replacements[$original] = $comment->uid;
+          break;
+
+        case 'pid':
+          $replacements[$original] = $comment->pid;
+          break;
+
+        // Poster identity information for comments
+        case 'hostname':
+          $replacements[$original] = $sanitize ? check_plain($comment->hostname) : $comment->hostname;
+          break;
+
+        case 'name':
+          $replacements[$original] = $sanitize ? filter_xss($comment->name) : $comment->name;
+          break;
+
+        case 'mail':
+          $replacements[$original] = $sanitize ? check_plain($comment->email) : $comment->mail;
+          break;
+
+        case 'homepage':
+          $replacements[$original] = $sanitize ? filter_xss_bad_protocol($comment->homepage) : $comment->homepage;
+          break;
+
+        case 'title':
+          $replacements[$original] = $sanitize ? filter_xss($comment->subject) : $comment->subject;
+          break;
+
+        case 'body':
+          $replacements[$original] = $sanitize ? check_markup($comment->comment, $comment->format) : $replacements[$original] = $comment->comment;
+          break;
+
+        // Comment related URLs.
+        case 'url':
+          $replacements[$original] = url('comment/' . $comment->cid, array('absolute' => TRUE, 'fragment' => 'comment-' . $comment->cid));
+          break;
+
+        case 'edit-url':
+          $replacements[$original] = url('comment/edit/' . $comment->cid, array('absolute' => TRUE));
+          break;
+
+        // Default values for the chained tokens handled below.
+        case 'author':
+          $replacements[$original] = $sanitize ? filter_xss($comment->name) : $comment->name;
+          break;
+
+        case 'parent':
+          if (!empty($comment->pid)) {
+            $parent = comment_load($comment->pid);
+            $replacements[$original] = $sanitize ? filter_xss($parent->subject) : $parent->subject;
+          }
+          break;
+
+        case 'created':
+          $replacements[$original] = format_date($comment->timestamp, 'medium', '', NULL, (isset($language) ? $language->language : NULL));
+          break;
+
+        case 'node':
+          $node = node_load($comment->nid);
+          $replacements[$original] = $sanitize ? filter_xss($node->title) : $node->title;
+          break;
+      }
+    }
+
+    // Chained token relationships.
+    if ($node_tokens = token_find_with_prefix($tokens, 'node')) {
+      $node = node_load($comment->nid);
+      $replacements += module_invoke_all('tokens', 'node', $node_tokens, array('node' => $node), $language, $sanitize);
+    }
+
+    if ($date_tokens = token_find_with_prefix($tokens, 'created')) {
+      $replacements += module_invoke_all('tokens', 'date', $date_tokens, array('date' => $comment->timestamp), $language, $sanitize);
+    }
+
+    if (($parent_tokens = token_find_with_prefix($tokens, 'parent')) && $parent = comment_load($comment->pid)) {
+      $replacements += module_invoke_all('tokens', 'comment', $parent_tokens, array('comment' => $parent), $language, $sanitize);
+    }
+
+    if (($author_tokens = token_find_with_prefix($tokens, 'author')) && $account = user_load($comment->uid)) {
+      $replacements += module_invoke_all('tokens', 'user', $author_tokens, array('user' => $account), $language, $sanitize);
+    }
+  }
+  elseif ($type == 'node' & !empty($data['node'])) {
+    $node = $data['node'];
+
+    foreach ($tokens as $name => $original) {
+      if ($name == 'comment-count') {
+        $replacements[$original] = $node->comment_count;
+      }
+    }
+  }
+
+  return $replacements;
+}
+
+/**
+ * Implement hook_token_info().
+ */
+function comment_token_info() {
+  $results = array();
+
+  // Metadata for token types.
+  $type['name'] = t('Comments');
+  $type['description'] = t('Tokens for comments posted on the site.');
+
+
+  // Comment-related tokens for nodes
+  $node['comment-count'] = array(
+    'name' => t("Comment count"),
+    'description' => t("The number of comments posted on a node."),
+  );
+
+
+  // Core comment tokens
+  $comment['cid'] = array(
+    'name' => t("Comment ID"),
+    'description' => t("The unique ID of the comment."),
+  );
+
+  $comment['pid'] = array(
+    'name' => t("Parent ID"),
+    'description' => t("The unique ID of the comment's parent, if comment threading is active."),
+  );
+
+  $comment['nid'] = array(
+    'name' => t("Node ID"),
+    'description' => t("The unique ID of the node the comment was posted to."),
+  );
+
+  $comment['uid'] = array(
+    'name' => t("User ID"),
+    'description' => t("The unique ID of the user who posted the comment."),
+  );
+
+  $comment['hostname'] = array(
+    'name' => t("IP Address"),
+    'description' => t("The IP address of the computer the comment was posted from."),
+  );
+
+  $comment['name'] = array(
+    'name' => t("Name"),
+    'description' => t("The name left by the comment author."),
+  );
+
+  $comment['mail'] = array(
+    'name' => t("Email address"),
+    'description' => t("The email address left by the comment author."),
+  );
+
+  $comment['homepage'] = array(
+    'name' => t("Home page"),
+    'description' => t("The home page URL left by the comment author."),
+  );
+
+  $comment['title'] = array(
+    'name' => t("Title"),
+    'description' => t("The title of the comment."),
+  );
+
+  $comment['body'] = array(
+    'name' => t("Content"),
+    'description' => t("The formatted content of the comment itself."),
+  );
+
+  $comment['url'] = array(
+    'name' => t("URL"),
+    'description' => t("The URL of the comment."),
+  );
+
+  $comment['edit-url'] = array(
+    'name' => t("Edit URL"),
+    'description' => t("The URL of the comment's edit page."),
+  );
+
+
+  // Chained tokens for comments
+  $comment['created'] = array(
+    'name' => t("Date created"),
+    'description' => t("The date the comment was posted."),
+    'references' => 'date',
+  );
+
+  $comment['parent'] = array(
+    'name' => t("Parent"),
+    'description' => t("The comment's parent, if comment threading is active."),
+    'references' => 'comment',
+  );
+
+  $comment['node'] = array(
+    'name' => t("Node"),
+    'description' => t("The node the comment was posted to."),
+    'references' => 'node',
+  );
+
+  $comment['author'] = array(
+    'name' => t("Author"),
+    'description' => t("The author of the comment, if they were logged in."),
+    'references' => 'user',
+  );
+
+  $results['types']['comment'] = $type;
+  $results['tokens']['node'] = $node;
+  $results['tokens']['comment'] = $comment;
+  return $results;
+}
Index: modules/node/node.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.info,v
retrieving revision 1.11
diff -u -p -r1.11 node.info
--- modules/node/node.info	8 Jun 2009 09:23:53 -0000	1.11
+++ modules/node/node.info	12 Jul 2009 19:55:28 -0000
@@ -10,4 +10,5 @@ files[] = node.admin.inc
 files[] = node.pages.inc
 files[] = node.install
 files[] = node.test
+files[] = node.tokens.inc
 required = TRUE
Index: modules/node/node.tokens.inc
===================================================================
RCS file: modules/node/node.tokens.inc
diff -N modules/node/node.tokens.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/node/node.tokens.inc	12 Jul 2009 19:55:28 -0000
@@ -0,0 +1,209 @@
+<?php
+// $Id: node.tokens.inc,v 1.16 2009/07/12 16:27:54 eaton Exp $
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for node-related data.
+ */
+
+/**
+ * Implement hook_tokens().
+ */
+function node_tokens($type, $tokens, $data, $language = NULL, $sanitize = TRUE) {
+  $url_options = array('absolute' => TRUE);
+  if (isset($language)) {
+    $url_options['language'] = $language;
+  }
+
+  $replacements = array();
+
+  if ($type == 'node' && !empty($data['node'])) {
+    $node = $data['node'];
+
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        // Simple key values on the node.
+        case 'nid':
+          $replacements[$original] = $node->nid;
+          break;
+
+        case 'vid':
+          $replacements[$original] = $node->vid;
+          break;
+
+        case 'tnid':
+          $replacements[$original] = $node->tnid;
+          break;
+
+        case 'uid':
+          $replacements[$original] = $node->uid;
+          break;
+
+        case 'name':
+          $replacements[$original] = $sanitize ? filter_xss($node->name) : $node->name;
+          break;
+
+        case 'title':
+          $replacements[$original] = $sanitize ? filter_xss($node->title) : $node->title;
+          break;
+
+        case 'body':
+          if (!empty($node->body)) {
+            $replacements[$original] = $sanitize ? $node->body[0]['safe'] : $node->body[0]['value'];
+          }
+          break;
+
+        case 'summary':
+          if (!empty($node->body)) {
+            $replacements[$original] = $sanitize ? $node->body[0]['safe_summary'] : $node->body[0]['summary'];
+          }
+          break;
+
+        case 'type':
+          $replacements[$original] = $sanitize ? check_plain($node->type) : $node->type;
+          break;
+
+        case 'type-name':
+          $type_name = node_get_types('name', $node->type);
+          $replacements[$original] = $sanitize ? check_plain($type_name) : $type_name;
+          break;
+
+        case 'language':
+          $replacements[$original] = $sanitize ? check_plain($node->language) : $node->language;
+          break;
+
+        case 'url':
+          $replacements[$original] = url('node/' . $node->nid, array('absolute' => TRUE));
+          break;
+
+        case 'edit-url':
+          $replacements[$original] = url('node/' . $node->nid . '/edit', array('absolute' => TRUE));
+          break;
+
+        // Default values for the chained tokens handled below.
+        case 'author':
+          $replacements[$original] = $sanitize ? filter_xss($node->name) : $node->name;
+          break;
+
+        case 'created':
+          $replacements[$original] = format_date($node->created, 'medium', '', NULL, (isset($language) ? $language->language : NULL));
+          break;
+
+        case 'changed':
+          $replacements[$original] = format_date($node->changed, 'medium', '', NULL, (isset($language) ? $language->language : NULL));
+          break;
+      }
+    }
+
+    if ($author_tokens = token_find_with_prefix($tokens, 'author')) {
+      $author = user_load($node->uid);
+      $replacements += module_invoke_all('tokens', 'user', $author_tokens, array('user' => $author), $language, $sanitize);
+    }
+
+    if ($created_tokens = token_find_with_prefix($tokens, 'created')) {
+      $replacements += module_invoke_all('tokens', 'date', $created_tokens, array('date' => $node->created), $language, $sanitize);
+    }
+
+    if ($changed_tokens = token_find_with_prefix($tokens, 'changed')) {
+      $replacements += module_invoke_all('tokens', 'date', $changed_tokens, array('date' => $node->changed), $language, $sanitize);
+    }
+  }
+
+  return $replacements;
+}
+
+/**
+ * Implement hook_token_info().
+ */
+function node_token_info() {
+  $results = array();
+
+  $type['name'] = t('Nodes');
+  $type['description'] = t('Tokens related to individual nodes.');
+
+
+  // Core tokens for nodes.
+  $node['nid'] = array(
+    'name' => t("Node ID"),
+    'description' => t("The unique ID of the node."),
+  );
+
+  $node['vid'] = array(
+    'name' => t("Revision ID"),
+    'description' => t("The unique ID of the node's latest revision."),
+  );
+
+  $node['tnid'] = array(
+    'name' => t("Translation set ID"),
+    'description' => t("The unique ID of the original-language version of this node, if one exists."),
+  );
+
+  $node['uid'] = array(
+    'name' => t("User ID"),
+    'description' => t("The unique ID of the user who posted the node."),
+  );
+
+  $node['type'] = array(
+    'name' => t("Content type"),
+    'description' => t("The type of the node."),
+  );
+
+  $node['type-name'] = array(
+    'name' => t("Content type name"),
+    'description' => t("The human-readable name of the node type."),
+  );
+
+  $node['title'] = array(
+    'name' => t("Title"),
+    'description' => t("The title of the node."),
+  );
+
+  $node['body'] = array(
+    'name' => t("Body"),
+    'description' => t("The main body text of the node."),
+  );
+
+  $node['summary'] = array(
+    'name' => t("Summary"),
+    'description' => t("The summary of the node's main body text."),
+  );
+
+  $node['language'] = array(
+    'name' => t("Language"),
+    'description' => t("The language the node is written in."),
+  );
+
+  $node['url'] = array(
+    'name' => t("URL"),
+    'description' => t("The URL of the node."),
+  );
+
+  $node['edit-url'] = array(
+    'name' => t("Edit URL"),
+    'description' => t("The URL of the node's edit page."),
+  );
+
+
+  // Chained tokens for nodes.
+  $node['created'] = array(
+    'name' => t("Date created"),
+    'description' => t("The date the node was posted."),
+    'references' => 'date',
+  );
+
+  $node['changed'] = array(
+    'name' => t("Date changed"),
+    'description' => t("The date the node was most recently updated."),
+    'references' => 'date',
+  );
+
+  $node['author'] = array(
+    'name' => t("Author"),
+    'description' => t("The author of the node."),
+    'references' => 'user',
+  );
+
+  $results['types']['node'] = $type;
+  $results['tokens']['node'] = $node;
+  return $results;
+}
Index: modules/poll/poll.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.info,v
retrieving revision 1.9
diff -u -p -r1.9 poll.info
--- modules/poll/poll.info	8 Jun 2009 09:23:53 -0000	1.9
+++ modules/poll/poll.info	12 Jul 2009 19:55:28 -0000
@@ -8,3 +8,4 @@ files[] = poll.module
 files[] = poll.pages.inc
 files[] = poll.install
 files[] = poll.test
+files[] = poll.tokens.inc
Index: modules/poll/poll.tokens.inc
===================================================================
RCS file: modules/poll/poll.tokens.inc
diff -N modules/poll/poll.tokens.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/poll/poll.tokens.inc	12 Jul 2009 19:55:28 -0000
@@ -0,0 +1,95 @@
+<?php
+// $Id: poll.tokens.inc,v 1.7 2009/07/12 16:27:54 eaton Exp $
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for values specific to Poll nodes.
+ */
+
+/**
+ * Implement hook_tokens().
+ */
+function poll_tokens($type, $tokens, $data, $language = NULL, $sanitize = TRUE) {
+  $replacements = array();
+
+  if ($type == 'node' && !empty($data['node']) && $data['node']->type == 'poll') {
+    $node = $data['node'];
+
+    $total_votes = 0;
+    $highest_votes = 0;
+    foreach ($node->choice as $choice) {
+      if ($choice['chvotes'] > $highest_votes) {
+        $winner = $choice;
+        $highest_votes = $choice['chvotes'];
+      }
+      $total_votes = $total_votes + $choice['chvotes'];
+    }
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        case 'poll-votes':
+          $replacements[$original] = $total_votes;
+          break;
+
+        case 'poll-winner':
+          if (isset($winner)) {
+            $replacements[$original] = $sanitize ? filter_xss($winner['chtext']) : $winner['chtext'];
+          }
+          break;
+
+        case 'poll-winner-votes':
+          if (isset($winner)) {
+            $replacements[$original] = $winner['chvotes'];
+          }
+          break;
+
+        case 'poll-winner-percent':
+          if (isset($winner)) {
+            $percent = ($winner['chvotes'] / $total_votes) * 100;
+            $replacements[$original] = number_format($percent, 0);
+          }
+          break;
+
+        case 'poll-duration':
+          $replacements[$original] = format_interval($node->runtime, 1, (isset($language) ? $language->language : NULL));
+          break;
+      }
+    }
+  }
+
+  return $replacements;
+}
+
+/**
+ * Implement hook_token_info().
+ */
+function poll_token_info() {
+  $results = array();
+
+  $node['poll-votes'] = array(
+    'name' => t("Poll votes"),
+    'description' => t("The number of votes that have been cast on a poll node."),
+  );
+
+  $node['poll-winner'] = array(
+    'name' => t("Poll winner"),
+    'description' => t("The winning poll answer."),
+  );
+
+  $node['poll-winner-votes'] = array(
+    'name' => t("Poll winner votes"),
+    'description' => t("The number of votes received by the winning poll answer."),
+  );
+
+  $node['poll-winner-percent'] = array(
+    'name' => t("Poll winner percent"),
+    'description' => t("The percentage of votes received by the winning poll answer."),
+  );
+
+  $node['poll-duration'] = array(
+    'name' => t("Poll duration"),
+    'description' => t("The length of time the poll node is set to run."),
+  );
+
+  $results['tokens']['node'] = $node;
+  return $results;
+}
Index: modules/statistics/statistics.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/statistics/statistics.info,v
retrieving revision 1.9
diff -u -p -r1.9 statistics.info
--- modules/statistics/statistics.info	8 Jun 2009 09:23:54 -0000	1.9
+++ modules/statistics/statistics.info	12 Jul 2009 19:55:28 -0000
@@ -9,3 +9,4 @@ files[] = statistics.admin.inc
 files[] = statistics.pages.inc
 files[] = statistics.install
 files[] = statistics.test
+files[] = statistics.tokens.inc
Index: modules/statistics/statistics.tokens.inc
===================================================================
RCS file: modules/statistics/statistics.tokens.inc
diff -N modules/statistics/statistics.tokens.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/statistics/statistics.tokens.inc	12 Jul 2009 19:55:28 -0000
@@ -0,0 +1,66 @@
+<?php
+// $Id: statistics.tokens.inc,v 1.8 2009/07/12 16:27:54 eaton Exp $
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for node visitor statistics.
+ */
+
+/**
+ * Implement hook_tokens().
+ */
+function statistics_tokens($type, $tokens, $data, $language = NULL, $sanitize = TRUE) {
+  $replacements = array();
+
+  if ($type == 'node' & !empty($data['node'])) {
+    $node = $data['node'];
+
+    foreach ($tokens as $name => $original) {
+      if ($name == 'views') {
+        $statistics = statistics_get($node->nid);
+        $replacements[$original] = $statistics['totalviews'];
+      }
+      elseif ($name == 'views-today') {
+        $statistics = statistics_get($node->nid);
+        $replacements[$original] = $statistics['dayviews'];
+      }
+      elseif ($name == 'last-view') {
+        $statistics = statistics_get($node->nid);
+        $replacements[$original] = format_date($statistics['timestamp']);
+      }
+    }
+
+    if ($created_tokens = token_find_with_prefix($tokens, 'last-view')) {
+      $statistics = statistics_get($node->nid);
+      $replacements += module_invoke_all('tokens', 'date', $created_tokens, array('date' => $statistics['timestamp']), $language, $sanitize);
+    }
+  }
+
+  return $replacements;
+}
+
+/**
+ * Implement hook_token_info().
+ */
+function statistics_token_info() {
+  $results = array();
+
+  $node['views'] = array(
+    'name' => t("Number of views"),
+    'description' => t("The number of visitors who have read the node."),
+  );
+
+  $node['day-views'] = array(
+    'name' => t("Views today"),
+    'description' => t("The number of visitors who have read the node today."),
+  );
+
+  $node['last-view'] = array(
+    'name' => t("Last view"),
+    'description' => t("The date on which a visitor last read the node."),
+    'references' => 'date',
+  );
+
+  $results['tokens']['node'] = $node;
+  return $results;
+}
Index: modules/system/system.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.info,v
retrieving revision 1.14
diff -u -p -r1.14 system.info
--- modules/system/system.info	17 Jun 2009 10:46:49 -0000	1.14
+++ modules/system/system.info	12 Jul 2009 19:55:28 -0000
@@ -11,4 +11,5 @@ files[] = image.gd.inc
 files[] = system.install
 files[] = system.test
 files[] = system.tar.inc
+files[] = system.tokens.inc
 required = TRUE
Index: modules/system/system.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.test,v
retrieving revision 1.54
diff -u -p -r1.54 system.test
--- modules/system/system.test	1 Jul 2009 08:39:55 -0000	1.54
+++ modules/system/system.test	12 Jul 2009 19:55:28 -0000
@@ -1062,3 +1062,45 @@ class QueueTestCase extends DrupalWebTes
     return $score;
   }
 }
+
+/**
+ * Test token replacement in strings.
+ */
+class TokenReplaceTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => t('Token replacement'),
+      'description' => t('Generates text using placeholders for dummy content to check token replacement.'),
+      'group' => t('System'),
+    );
+  }
+
+  /**
+   * Creates a user and a node, then tests the tokens generated from them.
+   */
+  function testTokenReplacement() {
+    // Create the initial objects.
+    $account = $this->drupalCreateUser();
+    $node = $this->drupalCreateNode(array('uid' => $account->uid));
+    global $user;
+
+    $source  = '[node:title]';
+    $source .= '[node:author:name]';
+    $source .= '[node:created:since]';
+    $source .= '[user:name]';
+    $source .= '[date:small]';
+    $source .= '[bogus:token]';
+
+    $target  = filter_xss($node->title);
+    $target .= filter_xss($account->name);
+    $target .= format_interval(REQUEST_TIME - $node->created, 2);
+    $target .= filter_xss($user->name);
+    $target .= format_date(REQUEST_TIME, 'small');
+    $target .= '[bogus:token]';
+
+    $result = token_replace($source, array('node' => $node));
+
+    // Check that the results of token_replace() match the expected values.
+    $this->assertFalse(strcmp($target, $result), t('Basic placeholder tokens handled.'));
+  }
+}
Index: modules/system/system.tokens.inc
===================================================================
RCS file: modules/system/system.tokens.inc
diff -N modules/system/system.tokens.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/system/system.tokens.inc	12 Jul 2009 19:55:28 -0000
@@ -0,0 +1,332 @@
+<?php
+// $Id: system.tokens.inc,v 1.16 2009/07/12 16:27:54 eaton Exp $
+
+/**
+ * @file
+ * Builds placeholder replacement tokens system-wide data.
+ *
+ * This file handles tokens for the global 'site' token type, as well as
+ * 'date' and 'file' tokens.
+ */
+
+/**
+ * Implement hook_tokens().
+ */
+function system_tokens($type, $tokens, $data, $language = NULL, $sanitize = TRUE) {
+  $url_options = array('absolute' => TRUE);
+  if (isset($language)) {
+    $url_options['language'] = $language;
+  }
+
+  $replacements = array();
+
+  if ($type == 'site') {
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        case 'name':
+          $site_name = variable_get('site_name', 'Drupal');
+          $replacements[$original] = $sanitize ? filter_xss($site_name) : $site_name;
+          break;
+
+        case 'slogan':
+          $slogan = variable_get('site_slogan');
+          $replacements[$original] = $sanitize ? filter_xss($slogan) : $slogan;
+          break;
+
+        case 'mission':
+          $mission = variable_get('site_mission');
+          $replacements[$original] = $sanitize ? filter_xss($mission) : $mission;
+          break;
+
+        case 'mail':
+          $mail = variable_get('site_name');
+          $replacements[$original] = variable_get('site_mail');
+          break;
+
+        case 'url':
+          $replacements[$original] = url('<front>', $url_options);
+          break;
+
+        case 'login-url':
+          $replacements[$original] = url('user', $url_options);
+          break;
+      }
+    }
+  }
+
+  elseif ($type == 'date') {
+    if (empty($data['date'])) {
+      $date = REQUEST_TIME;
+    }
+    else {
+      $date = $data['date'];
+    }
+    $langcode = (isset($language) ? $language->language : NULL);
+
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        case 'raw':
+          $replacements[$original] = filter_xss($date);
+          break;
+
+        case 'small':
+          $replacements[$original] = format_date($date, 'small', '', NULL, $langcode);
+          break;
+
+        case 'medium':
+          $replacements[$original] = format_date($date, 'medium', '', NULL, $langcode);
+          break;
+
+        case 'large':
+          $replacements[$original] = format_date($date, 'large', '', NULL, $langcode);
+          break;
+
+        case 'since':
+          $replacements[$original] = format_interval((REQUEST_TIME - $date), 2, $langcode);
+          break;
+      }
+    }
+
+    if ($created_tokens = token_find_with_prefix($tokens, 'custom')) {
+      foreach ($created_tokens as $name => $original) {
+        $replacements[$original] = format_date($date, 'custom', $name, NULL, $langcode);
+      }
+    }
+  }
+
+  elseif ($type == 'file' && !empty($data['file'])) {
+    $file = $data['file'];
+
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        // Basic keys and values.
+        case 'fid':
+          $replacements[$original] = $file->fid;
+          break;
+
+        case 'uid':
+          $replacements[$original] = $file->uid;
+          break;
+
+        case 'nid':
+          $replacements[$original] = $file->nid;
+          break;
+
+        // Essential file data
+        case 'name':
+          $replacements[$original] = $sanitize ? check_plain($file->filename) : $file->filename;
+          break;
+
+        case 'description':
+          $replacements[$original] = $sanitize ? filter_xss($file->description) : $file->description;
+          break;
+
+        case 'path':
+          $replacements[$original] = $sanitize ? filter_xss($file->filepath) : $file->filepath;
+          break;
+
+        case 'mime':
+          $replacements[$original] = $sanitize ? filter_xss($file->filemime) : $file->filemime;
+          break;
+
+        case 'size':
+          $replacements[$original] = format_size($file->filesize);
+          break;
+
+        case 'url':
+          $replacements[$original] = url(file_create_url($file->filepath), $url_options);
+          break;
+
+        // These tokens are default variations on the chained tokens handled below.
+        case 'node':
+          if ($nid = $file->nid) {
+            $node = node_load($file->nid);
+            $replacements[$original] = $sanitize ? filter_xss($node->title) : $node->title;
+          }
+          break;
+
+        case 'timestamp':
+          $replacements[$original] = format_date($file->timestamp, 'medium', '', NULL, (isset($language) ? $language->language : NULL));
+          break;
+
+        case 'owner':
+          $account = user_load($file->uid);
+          $replacements[$original] = $sanitize ? filter_xss($user->name) : $user->name;
+          break;
+      }
+    }
+
+    if ($node_tokens = token_find_with_prefix($tokens, 'node')) {
+      $node = node_load($file->nid);
+      $replacements += module_invoke_all('tokens', 'node', $node_tokens, array('node' => $node), $language, $sanitize);
+    }
+
+    if ($date_tokens = token_find_with_prefix($tokens, 'timestamp')) {
+      $replacements += module_invoke_all('tokens', 'date', $date_tokens, array('date' => $file->timestamp), $language, $sanitize);
+    }
+
+    if (($owner_tokens = token_find_with_prefix($tokens, 'owner')) && $account = user_load($file->uid)) {
+      $replacements += module_invoke_all('tokens', 'user', $owner_tokens, array('user' => $account), $language, $sanitize);
+    }
+  }
+
+  return $replacements;
+}
+
+/**
+ * Implement hook_token_info().
+ */
+function system_token_info() {
+  $results = array();
+
+  $types['site'] = array(
+    'name' => t("Site information"),
+    'description' => t("Tokens for site-wide settings and other global information."),
+  );
+
+  $types['date'] = array(
+    'name' => t("Dates"),
+    'description' => t("Tokens related to times and dates."),
+  );
+
+  $types['file'] = array(
+    'name' => t("Files"),
+    'description' => t("Tokens related to uploaded files."),
+  );
+
+
+  // Site-wide global tokens.
+  $site['name'] = array(
+    'name' => t("Name"),
+    'description' => t("The name of the site."),
+  );
+
+  $site['slogan'] = array(
+    'name' => t("Slogan"),
+    'description' => t("The slogan of the site."),
+  );
+
+  $site['mission'] = array(
+    'name' => t("Mission"),
+    'description' => t("The optional 'mission' of the site."),
+  );
+
+  $site['mail'] = array(
+    'name' => t("Email"),
+    'description' => t("The administrative email address for the site."),
+  );
+
+  $site['url'] = array(
+    'name' => t("URL"),
+    'description' => t("The URL of the site's front page."),
+  );
+
+  $site['login-url'] = array(
+    'name' => t("Login page"),
+    'description' => t("The URL of the site's login page."),
+  );
+
+
+  // Date related tokens.
+  $date['small'] = array(
+    'name' => t("Small format"),
+    'description' => t("A date in 'small' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'small'))),
+  );
+
+  $date['medium'] = array(
+    'name' => t("Medium format"),
+    'description' => t("A date in 'medium' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'medium'))),
+  );
+
+  $date['large'] = array(
+    'name' => t("Large format"),
+  );
+  $date['large']['description'] = t("A date in 'large' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'large')));
+
+  $date['custom'] = array(
+    'name' => t("Custom format"),
+    'description' => t("A date in a custom format. See !php-date for details.", array('!php-date' => l(t('the PHP documentation'), 'http://php.net/manual/en/function.date.php'))),
+  );
+
+  $date['since'] = array(
+    'name' => t("Time-since"),
+    'description' => t("A data in 'time-since' format. (%date)", array('%date' => format_interval(REQUEST_TIME - 360, 2))),
+  );
+
+  $date['raw'] = array(
+    'name' => t("Raw timestamp"),
+    'description' => t("A date in UNIX timestamp format (%date)", array('%date' => REQUEST_TIME)),
+  );
+
+
+  // File related tokens.
+  $file['fid'] = array(
+    'name' => t("File ID"),
+    'description' => t("The unique ID of the uploaded file."),
+  );
+
+  $file['uid'] = array(
+    'name' => t("User ID"),
+    'description' => t("The unique ID of the user who owns the file."),
+  );
+
+  $file['nid'] = array(
+    'name' => t("Node ID"),
+    'description' => t("The unique ID of the node the file is attached to."),
+  );
+
+  $file['name'] = array(
+    'name' => t("File name"),
+    'description' => t("The name of the file on disk."),
+  );
+
+  $file['description'] = array(
+    'name' => t("Description"),
+    'description' => t("An optional human-readable description of the file."),
+  );
+
+  $file['path'] = array(
+    'name' => t("Path"),
+    'description' => t("The location of the file on disk."),
+  );
+
+  $file['mime'] = array(
+    'name' => t("MIME type"),
+    'description' => t("The MIME type of the file."),
+  );
+
+  $file['size'] = array(
+    'name' => t("File size"),
+    'description' => t("The size of the file, in kilobytes."),
+  );
+
+  $file['path'] = array(
+    'name' => t("URL"),
+    'description' => t("The web-accessible URL for the file."),
+  );
+
+  $file['timestamp'] = array(
+    'name' => t("Timestamp"),
+    'description' => t("The date the file was most recently changed."),
+    'references' => 'date',
+  );
+
+  $file['node'] = array(
+    'name' => t("Node"),
+    'description' => t("The node the file is attached to."),
+    'references' => 'date',
+  );
+
+  $file['owner'] = array(
+    'name' => t("Owner"),
+    'description' => t("The user who originally uploaded the file."),
+    'references' => 'user',
+  );
+
+
+  $results['types'] = $types;
+  $results['tokens']['site'] = $site;
+  $results['tokens']['date'] = $date;
+  $results['tokens']['file'] = $file;
+  return $results;
+}
Index: modules/taxonomy/taxonomy.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.info,v
retrieving revision 1.9
diff -u -p -r1.9 taxonomy.info
--- modules/taxonomy/taxonomy.info	8 Jun 2009 09:23:54 -0000	1.9
+++ modules/taxonomy/taxonomy.info	12 Jul 2009 19:55:28 -0000
@@ -9,3 +9,4 @@ files[] = taxonomy.admin.inc
 files[] = taxonomy.pages.inc
 files[] = taxonomy.install
 files[] = taxonomy.test
+files[] = taxonomy.tokens.inc
Index: modules/taxonomy/taxonomy.tokens.inc
===================================================================
RCS file: modules/taxonomy/taxonomy.tokens.inc
diff -N modules/taxonomy/taxonomy.tokens.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/taxonomy/taxonomy.tokens.inc	12 Jul 2009 19:55:28 -0000
@@ -0,0 +1,201 @@
+<?php
+// $Id: taxonomy.tokens.inc,v 1.12 2009/07/12 16:27:54 eaton Exp $
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for taxonomy terms and vocabularies.
+ */
+
+/**
+ * Implement hook_tokens().
+ */
+function taxonomy_tokens($type, $tokens, $data, $language = NULL, $sanitize = TRUE) {
+  $replacements = array();
+
+  if ($type == 'term' && !empty($data['term'])) {
+    $term = $data['term'];
+
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        case 'tid':
+          $replacements[$original] = $term->tid;
+          break;
+
+        case 'vid':
+          $replacements[$original] = $term->vid;
+          break;
+
+        case 'name':
+          $replacements[$original] = $sanitize ? filter_xss($term->name) : $term->name;
+          break;
+
+        case 'description':
+          $replacements[$original] = $sanitize ? filter_xss($term->description) : $term->description;
+          break;
+
+        case 'url':
+          $replacements[$original] = url('taxonomy/term/' . $term->tid, array('absolute' => TRUE));
+          break;
+
+        case 'node-count':
+          $sql = "SELECT COUNT (1) FROM {taxonomy_term_node} tn WHERE tn.tid = :tid";
+          $count = db_result(db_query($sql, array(':tid' => $term->tid)));
+          $replacements[$original] = $count;
+          break;
+
+        case 'vocabulary':
+          $vocabulary = taxonomy_vocabulary_load($term->vid);
+          $replacements[$original] = filter_xss($vocabulary->name);
+          break;
+
+        case 'parent':
+          $parents = taxonomy_get_parents($term->tid);
+          $parent = array_pop($parents);
+          $replacements[$original] = filter_xss($parent->name);
+          break;
+      }
+    }
+
+    if ($vocabulary_tokens = token_find_with_prefix($tokens, 'vocabulary')) {
+      $vocabulary = taxonomy_vocabulary_load($term->vid);
+      $replacements += module_invoke_all('tokens', 'vocabulary', $vocabulary_tokens, array('vocabulary' => $vocabulary), $language, $sanitize);
+    }
+
+    if ($vocabulary_tokens = token_find_with_prefix($tokens, 'parent')) {
+      $parents = taxonomy_get_parents($term->tid);
+      $parent = array_pop($parents);
+      $replacements += module_invoke_all('tokens', 'term', $vocabulary_tokens, array('term' => $parent), $language, $sanitize);
+    }
+  }
+
+  elseif ($type == 'vocabulary' && !empty($data['vocabulary'])) {
+    $vocabulary = $data['vocabulary'];
+
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        case 'vid':
+          $replacements[$original] = $vocabulary->vid;
+          break;
+
+        case 'name':
+          $replacements[$original] = $sanitize ? filter_xss($vocabulary->name) : $vocabulary->name;
+          break;
+
+        case 'description':
+          $replacements[$original] = $sanitize ? filter_xss($vocabulary->description) : $vocabulary->description;
+          break;
+
+        case 'term-count':
+          $sql = "SELECT COUNT (1) FROM {taxonomy_term_data} td WHERE td.vid = :vid";
+          $count = db_result(db_query($sql, array(':vid' => $vocabulary->vid)));
+          $replacements[$original] = $count;
+          break;
+
+        case 'node-count':
+          $sql = "SELECT COUNT (1) FROM {taxonomy_term_node} tn LEFT JOIN {taxonomy_term_data} td ON tn.tid = td.tid WHERE td.vid = :vid";
+          $count = db_result(db_query($sql, array(':vid' => $vocabulary->vid)));
+          $replacements[$original] = $count;
+          break;
+      }
+    }
+  }
+
+  return $replacements;
+}
+
+/**
+ * Implement hook_token_info().
+ */
+function taxonomy_token_info() {
+  $results = array();
+
+  // Metadata for token types.
+  $types['term'] = array(
+    'name' => t("Taxonomy terms"),
+    'description' => t("Tokens related to taxonomy terms."),
+  );
+
+  $types['vocabulary'] = array(
+    'name' => t("Vocabularies"),
+    'description' => t("Tokens related to taxonomy vocabularies."),
+  );
+
+
+  // Taxonomy term related variables.
+  $term['tid'] = array(
+    'name' => t("Term ID"),
+    'description' => t("The unique ID of the taxonomy term."),
+  );
+
+  $term['vid'] = array(
+    'name' => t("Vocabulary ID"),
+    'description' => t("The unique ID of the vocabulary the term belongs to."),
+  );
+
+  $term['name'] = array(
+    'name' => t("Name"),
+    'description' => t("The name of the taxonomy term."),
+  );
+
+  $term['description'] = array(
+    'name' => t("Description"),
+    'description' => t("The optional description of the taxonomy term."),
+  );
+
+  $term['node-count'] = array(
+    'name' => t("Node count"),
+    'description' => t("The number of nodes tagged with the taxonomy term."),
+  );
+
+  $term['url'] = array(
+    'name' => t("URL"),
+    'description' => t("The URL of the taxonomy term."),
+  );
+
+
+  // Taxonomy vocabulary related variables.
+  $vocabulary['vid'] = array(
+    'name' => t("Vocabulary ID"),
+    'description' => t("The unique ID of the taxonomy vocabulary."),
+  );
+
+  $vocabulary['name'] = array(
+    'name' => t("Name"),
+    'description' => t("The name of the taxonomy vocabulary."),
+  );
+
+  $vocabulary['description'] = array(
+    'name' => t("Description"),
+    'description' => t("The optional description of the taxonomy vocabulary."),
+  );
+
+  $vocabulary['node-count'] = array(
+    'name' => t("Node count"),
+    'description' => t("The number of nodes tagged with terms belonging to the taxonomy vocabulary."),
+  );
+
+  $vocabulary['term-count'] = array(
+    'name' => t("Node count"),
+    'description' => t("The number of terms belonging to the taxonomy vocabulary."),
+  );
+
+
+  // Chained tokens for taxonomies
+  $term['vocabulary'] = array(
+    'name' => t("Vocabulary"),
+    'description' => t("The vocabulary the taxonomy term belongs to."),
+    'references' => 'vocabulary',
+  );
+
+  $term['parent'] = array(
+    'name' => t("Parent term"),
+    'description' => t("The parent term of the taxonomy term, if one exists."),
+    'references' => 'term',
+  );
+
+  $results['tokens']['term'] = $term;
+  $results['tokens']['vocabulary'] = $vocabulary;
+  $results['types'] = $types;
+
+  return $results;
+}
Index: modules/upload/upload.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/upload/upload.info,v
retrieving revision 1.9
diff -u -p -r1.9 upload.info
--- modules/upload/upload.info	8 Jun 2009 09:23:54 -0000	1.9
+++ modules/upload/upload.info	12 Jul 2009 19:55:28 -0000
@@ -8,3 +8,4 @@ files[] = upload.module
 files[] = upload.admin.inc
 files[] = upload.install
 files[] = upload.test
+files[] = upload.tokens.inc
Index: modules/upload/upload.tokens.inc
===================================================================
RCS file: modules/upload/upload.tokens.inc
diff -N modules/upload/upload.tokens.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/upload/upload.tokens.inc	12 Jul 2009 19:55:28 -0000
@@ -0,0 +1,42 @@
+<?php
+// $Id: upload.tokens.inc,v 1.9 2009/07/12 16:27:54 eaton Exp $
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for uploaded files attached to nodes.
+ */
+
+/**
+ * Implement hook_tokens().
+ */
+function upload_tokens($type, $tokens, $data, $language = NULL, $sanitize = TRUE) {
+  $replacements = array();
+
+  if ($type == 'node' && !empty($data['node'])) {
+    $node = $data['node'];
+
+    foreach ($tokens as $name => $original) {
+      if ($name == 'upload') {
+        $upload = array_shift($node->files);
+        $replacements[$original] = file_create_url($upload->filepath);
+      }
+    }
+
+    if (($upload_tokens = token_find_with_prefix($tokens, 'upload')) && !empty($node->files) && $upload = array_shift($node->files)) {
+      $replacements += module_invoke_all('tokens', 'file', $upload_tokens, array('file' => $upload), $language, $sanitize);
+    }
+  }
+
+  return $replacements;
+}
+
+function upload_token_info() {
+  $results['tokens']['node'] = array(
+  'upload' => array(
+    'name' => t('File attachment'),
+    'description' => t('The first file attached to a node, if one exists.'),
+    'references' => 'file',
+    )
+  );
+  return $results;
+}
\ No newline at end of file
Index: modules/user/user.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.info,v
retrieving revision 1.11
diff -u -p -r1.11 user.info
--- modules/user/user.info	8 Jun 2009 09:23:55 -0000	1.11
+++ modules/user/user.info	12 Jul 2009 19:55:28 -0000
@@ -9,4 +9,5 @@ files[] = user.admin.inc
 files[] = user.pages.inc
 files[] = user.install
 files[] = user.test
+files[] = user.tokens.inc
 required = TRUE
Index: modules/user/user.tokens.inc
===================================================================
RCS file: modules/user/user.tokens.inc
diff -N modules/user/user.tokens.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/user/user.tokens.inc	12 Jul 2009 19:55:28 -0000
@@ -0,0 +1,155 @@
+<?php
+// $Id: user.tokens.inc,v 1.16 2009/07/12 16:27:54 eaton Exp $
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for user-related data.
+ */
+
+/**
+ * Implement hook_tokens().
+ */
+function user_tokens($type, $tokens, $data, $language = NULL, $sanitize = TRUE) {
+  global $user;
+  $url_options = array('absolute' => TRUE);
+  if (isset($language)) {
+    $url_options['language'] = $language;
+  }
+
+  $replacements = array();
+
+  if ($type == 'user') {
+    if (!empty($data['user'])) {
+      $account = $data['user'];
+    }
+    else {
+      $account = $user;
+    }
+
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        // Basic user account information.
+        case 'uid':
+          $replacements[$original] = $account->uid;
+          break;
+
+        case 'name':
+          $replacements[$original] = $sanitize ? filter_xss($account->name) : $account->name;
+          break;
+
+        case 'mail':
+          $replacements[$original] = $sanitize ? filter_xss($account->mail) : $account->mail;
+          break;
+
+        case 'new-password':
+          $replacements[$original] = $account->password;
+          break;
+
+        case 'url':
+          $replacements[$original] = url("user/$account->uid", $url_options);
+          break;
+
+        case 'edit-url':
+          $replacements[$original] = url("user/$account->uid/edit", $url_options);
+          break;
+
+        case 'confirm-cancel-url':
+          $replacements[$original] = user_cancel_url($account);
+          break;
+
+        case 'password-reset-url':
+          $replacements[$original] = user_pass_reset_url($account);
+          break;
+
+        // These tokens are default variations on the chained tokens handled below.
+        case 'last-login':
+          $replacements[$original] = format_date($account->login, 'medium', '', NULL, (isset($language) ? $language->language : NULL));
+          break;
+
+        case 'created':
+          $replacements[$original] = format_date($account->created, 'medium', '', NULL, (isset($language) ? $language->language : NULL));
+          break;
+      }
+    }
+
+    if ($login_tokens = token_find_with_prefix($tokens, 'last-login')) {
+      $replacements += module_invoke_all('tokens', 'date', $login_tokens, array('date' => $account->login), $language, $sanitize);
+    }
+
+    if ($registered_tokens = token_find_with_prefix($tokens, 'created')) {
+      $replacements += module_invoke_all('tokens', 'date', $registered_tokens, array('date' => $account->created), $language, $sanitize);
+    }
+  }
+
+  return $replacements;
+}
+
+
+function user_token_info() {
+  $results = array();
+
+  $type = array(
+    'name' => t('Users'),
+    'description' => t('Tokens related to individual user accounts.'),
+  );
+
+
+  $user['uid'] = array(
+    'name' => t('User ID'),
+    'description' => t("The unique ID of the user account."),
+  );
+
+  $user['name'] = array(
+    'name' => t("Name"),
+    'description' => t("The login name of the user account."),
+  );
+
+  $user['mail'] = array(
+    'name' => t("Email"),
+    'description' => t("The email address of the user account."),
+  );
+
+  $user['new-password'] = array(
+    'name' => t("New password"),
+    'description' => t("A newly-assigned password given to the user account on creation."),
+  );
+
+  $user['url'] = array(
+    'name' => t("URL"),
+    'description' => t("The URL of the account profile page."),
+  );
+
+  $user['edit-url'] = array(
+    'name' => t("Edit URL"),
+    'description' => t("The url of the account edit page."),
+  );
+
+  $user['confirm-cancel-url'] = array(
+    'name' => t("Account cancellation URL"),
+    'description' => t("The account cancellation URL for the user account. Visiting this URL will automatically cancel a user account: it cannot be undone."),
+  );
+
+  $user['password-reset-url'] = array(
+    'name' => t("Password reset URL"),
+    'description' => t("A single-use password reset URL for the user account."),
+  );
+
+
+  // Chained tokens for users.
+  $user['last-login'] = array(
+    'name' => t("Last login"),
+    'description' => t("The date the user last logged in to the site."),
+    'references' => 'date',
+  );
+
+  $user['created'] = array(
+    'name' => t("Created"),
+    'description' => t("The date the user account was created."),
+    'references' => 'date',
+  );
+
+
+  $results['types']['user'] = $type;
+  $results['tokens']['user'] = $user;
+  return $results;
+}
