From 40003c8307ca64da0cd1ab0ee4d87f4812be8088 Mon Sep 17 00:00:00 2001
From: Dries Buytaert <dries@buytaert.net>
Date: Wed, 19 Aug 2009 20:19:37 +0000
Subject: [PATCH] - Patch #113614 by eaton, fago, et al: add centralized
 token/placeholder subsituation to core.

---
 includes/common.inc                      |   1 +
 includes/token.inc                       | 238 ++++++++++++++++++
 modules/comment/comment.info             |   1 +
 modules/comment/comment.tokens.inc       | 248 ++++++++++++++++++
 modules/node/node.info                   |   1 +
 modules/node/node.tokens.inc             | 205 +++++++++++++++
 modules/poll/poll.info                   |   1 +
 modules/poll/poll.tokens.inc             |  91 +++++++
 modules/statistics/statistics.info       |   1 +
 modules/statistics/statistics.tokens.inc |  63 +++++
 modules/system/system.info               |   1 +
 modules/system/system.module             | 150 ++---------
 modules/system/system.test               |  57 +++++
 modules/system/system.tokens.inc         | 308 +++++++++++++++++++++++
 modules/taxonomy/taxonomy.info           |   1 +
 modules/taxonomy/taxonomy.tokens.inc     | 189 ++++++++++++++
 modules/upload/upload.info               |   1 +
 modules/upload/upload.tokens.inc         |  45 ++++
 modules/user/user.info                   |   1 +
 modules/user/user.tokens.inc             | 129 ++++++++++
 20 files changed, 1598 insertions(+), 134 deletions(-)
 create mode 100644 includes/token.inc
 create mode 100644 modules/comment/comment.tokens.inc
 create mode 100644 modules/node/node.tokens.inc
 create mode 100644 modules/poll/poll.tokens.inc
 create mode 100644 modules/statistics/statistics.tokens.inc
 create mode 100644 modules/system/system.tokens.inc
 create mode 100644 modules/taxonomy/taxonomy.tokens.inc
 create mode 100644 modules/upload/upload.tokens.inc
 create mode 100644 modules/user/user.tokens.inc

diff --git a/includes/common.inc b/includes/common.inc
index f6e64cf47207..4e701de9e548 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -3549,6 +3549,7 @@ function _drupal_bootstrap_full() {
   require_once DRUPAL_ROOT . '/includes/mail.inc';
   require_once DRUPAL_ROOT . '/includes/actions.inc';
   require_once DRUPAL_ROOT . '/includes/ajax.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');
diff --git a/includes/token.inc b/includes/token.inc
new file mode 100644
index 000000000000..7f786121d95f
--- /dev/null
+++ b/includes/token.inc
@@ -0,0 +1,238 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Drupal placeholder/token replacement system.
+ *
+ * Provides a set of extensible API functions for replacing placeholders in text
+ * with meaningful values.
+ *
+ * For example: When configuring automated emails, an administrator enters standard
+ * text for the email. Variables like the title of a node and the date the email
+ * was sent can be entered as placeholders like [node:title] and [date:short].
+ * When a Drupal module prepares to send the email, it can call the token_replace()
+ * function, passing in the text. The token system will scan the text for placeholder
+ * tokens, give other modules an opportunity to replace them with meaningful text,
+ * then return the final product to the original module.
+ *
+ * Tokens follow the form: [$type:$name], where $type is a general class of
+ * tokens like 'node', 'user', or 'comment' and $name is the name of a given
+ * placeholder. For example, [node:title].
+ *
+ * In addition to raw text containing placeholders, modules may pass in an array
+ * of objects to be used when performing the replacement. The objects should be
+ * keyed by the token type they correspond to. For example:
+ *
+ * @code
+ * // Load a node and a user, then replace tokens in the text.
+ * $text = 'On [date:short], [user:name] read [node:title].';
+ * $node = node_load(1);
+ * $user = user_load(1);
+ *
+ * // [date:...] tokens use the current date automatically.
+ * $data = array('node' => $node, 'user' => $user);
+ * return token_replace($text, $data);
+ * @endcode
+ *
+ * Some tokens may be chained in the form of [$type:$pointer:$name], where $type
+ * is a normal token type, $pointer is a reference to another token type, and
+ * $name is the name of a given placeholder. For example, [node:author:mail]. In
+ * that example, 'author' is a pointer to the 'user' account that created the node,
+ * and 'mail' is a placeholder available for any 'user'.
+ *
+ * @see token_replace()
+ * @see hook_tokens()
+ * @see hook_token_info()
+ */
+
+/**
+ * Replace all tokens in a given string with appropriate values.
+ *
+ * @param $text
+ *   A string potentially containing replacable tokens.
+ * @param $data
+ *   (optional) 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 it is empty.
+ * @param $options
+ *   (optional) A keyed array of settings and flags to control the token
+ *   replacement process. Supported options are:
+ *   - language: A language object to be used when generating locale-sensitive
+ *     tokens.
+ *   - callback: A callback function that will be used to post-process the array
+ *     of token replacements after they are generated. For example, a module using
+ *     tokens in a text-only email might provide a callback to strip HTML
+ *     entities from token values before they are inserted into the final text.
+ *   - clear: A boolean flag indicating that tokens should be removed from the
+ *     final text if no replacement value can be generated.
+ *   - sanitize: A boolean flag indicating that tokens should be sanitized for
+ *     display to a web browser. Defaults to TRUE. Developers who set this option
+ *     to FALSE assume responsibility for running filter_xss(), check_plain() or
+ *     other appropriate scrubbing functions before displaying data to users.
+ * @return
+ *   Text with tokens replaced.
+ */
+function token_replace($text, array $data = array(), array $options = array()) {
+  $replacements = array();
+  foreach (token_scan($text) as $type => $tokens) {
+    $replacements += token_generate($type, $tokens, $data, $options);
+  }
+
+  // Optionally alter the list of replacement values.
+  if (!empty($options['callback']) && drupal_function_exists($options['callback'])) {
+    $function = $options['callback'];
+    $function($replacements, $data, $options);
+  }
+
+  $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 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 $type
+ *   The type of token being replaced. 'node', 'user', and 'date' are common.
+ * @param $tokens
+ *   An array of tokens to be replaced, keyed by the literal text of the token
+ *   as it appeared in the source text.
+ * @param $data
+ *   (optional) 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 it is empty.
+ * @param $options
+ *   (optional) A keyed array of settings and flags to control the token
+ *   replacement process. Supported options are:
+ *   - 'language' A language object to be used when generating locale-sensitive
+ *     tokens.
+ *   - 'callback' A callback function that will be used to post-process the array
+ *     of token replacements after they are generated. Can be used when modules
+ *     require special formatting of token text, for example URL encoding or
+ *     truncation to a specific length.
+ *   - 'sanitize' A boolean flag indicating that tokens should be sanitized for
+ *     display to a web browser. Developers who set this option to FALSE assume
+ *     responsibility for running filter_xss(), check_plain() or other
+ *     appropriate scrubbing functions before displaying data to users.
+ * @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($type, array $tokens, array $data = array(), array $options = array()) {
+  $results = array();
+  $options += array('sanitize' => TRUE);
+
+  foreach (module_implements('tokens') as $module) {
+    $function = $module . '_tokens';
+    if (drupal_function_exists($function)) {
+      $result = $function($type, $tokens, $data, $options);
+      foreach ($result 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. For example:
+ * @code
+ *   $data = array(
+ *     'author:name' => '[node:author:name]',
+ *     'title'       => '[node:title]',
+ *     'created'     => '[node:author:name]',
+ *   );
+ *   $results = token_find_with_prefix($data, 'author');
+ *   $results == array('name' => '[node:author:name]');
+ * @endcode
+ *
+ * @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:
+ * @code
+ *   $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.'),
+ *     'type' => 'user',
+ *   );
+ * @endcode
+ * @return
+ *   An associative array of token information, grouped by token type.
+ */
+function token_info() {
+  $data = &drupal_static(__FUNCTION__);
+  if (!isset($data)) {
+    $data = module_invoke_all('token_info');
+    drupal_alter('token_info', $data);
+  }
+  return $data;
+}
diff --git a/modules/comment/comment.info b/modules/comment/comment.info
index 71a62e3ed44b..5b6a48acf355 100644
--- a/modules/comment/comment.info
+++ b/modules/comment/comment.info
@@ -10,3 +10,4 @@ files[] = comment.admin.inc
 files[] = comment.pages.inc
 files[] = comment.install
 files[] = comment.test
+files[] = comment.tokens.inc
diff --git a/modules/comment/comment.tokens.inc b/modules/comment/comment.tokens.inc
new file mode 100644
index 000000000000..09b89116b9a6
--- /dev/null
+++ b/modules/comment/comment.tokens.inc
@@ -0,0 +1,248 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for comment-related data.
+ */
+
+/**
+ * Implement hook_token_info().
+ */
+function comment_token_info() {
+  $type = array(
+    'name' => t('Comments'),
+    'description' => t('Tokens for comments posted on the site.'),
+    'needs-data' => 'comment',
+  );
+
+  // Comment-related tokens for nodes
+  $node['comment-count'] = array(
+    'name' => t("Comment count"),
+    'description' => t("The number of comments posted on a node."),
+  );
+  $node['comment-count-new'] = array(
+    'name' => t("New comment count"),
+    'description' => t("The number of comments posted on a node since the reader last viewed it."),
+  );
+
+  // 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."),
+    'type' => 'date',
+  );
+  $comment['parent'] = array(
+    'name' => t("Parent"),
+    'description' => t("The comment's parent, if comment threading is active."),
+    'type' => 'comment',
+  );
+  $comment['node'] = array(
+    'name' => t("Node"),
+    'description' => t("The node the comment was posted to."),
+    'type' => 'node',
+  );
+  $comment['author'] = array(
+    'name' => t("Author"),
+    'description' => t("The author of the comment, if they were logged in."),
+    'type' => 'user',
+  );
+
+  return array(
+    'types' => array('comment' => $type),
+    'tokens' => array(
+      'node' => $node,
+      'comment' => $comment,
+    ),
+  );
+}
+
+/**
+ * Implement hook_tokens().
+ */
+function comment_tokens($type, $tokens, array $data = array(), array $options = array()) {
+  $url_options = array('absolute' => TRUE);
+  if (isset($options['language'])) {
+    $url_options['language'] = $language;
+    $language_code = $language->language;
+  }
+  else {
+    $language_code = NULL;
+  }
+  $sanitize = !empty($options['sanitize']);
+
+  $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':
+          $name = ($comment->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $comment->name;
+          $replacements[$original] = $sanitize ? filter_xss($name) : $name;
+          break;
+
+        case 'mail':
+          if ($comment->uid != 0) {
+            $account = user_load($comment->uid);
+            $mail = $account->mail;
+          }
+          else {
+            $mail = $comment->mail;
+          }
+          $replacements[$original] = $sanitize ? check_plain($mail) : $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, $language_code);
+          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 += token_generate('node', $node_tokens, array('node' => $node), $options);
+    }
+
+    if ($date_tokens = token_find_with_prefix($tokens, 'created')) {
+      $replacements += token_generate('date', $date_tokens, array('date' => $comment->timestamp), $options);
+    }
+
+    if (($parent_tokens = token_find_with_prefix($tokens, 'parent')) && $parent = comment_load($comment->pid)) {
+      $replacements += token_generate('comment', $parent_tokens, array('comment' => $parent), $options);
+    }
+
+    if (($author_tokens = token_find_with_prefix($tokens, 'author')) && $account = user_load($comment->uid)) {
+      $replacements += token_generate('user', $author_tokens, array('user' => $account), $options);
+    }
+  }
+  elseif ($type == 'node' & !empty($data['node'])) {
+    $node = $data['node'];
+
+    foreach ($tokens as $name => $original) {
+      switch($name) {
+        case 'comment-count':
+          $replacements[$original] = $node->comment_count;
+          break;
+
+        case 'comment-count-new':
+          $replacements[$original] = comment_num_new($node->nid);
+          break;
+      }
+    }
+  }
+
+  return $replacements;
+}
diff --git a/modules/node/node.info b/modules/node/node.info
index 2f62a99013b4..300213411122 100644
--- a/modules/node/node.info
+++ b/modules/node/node.info
@@ -10,4 +10,5 @@ files[] = node.admin.inc
 files[] = node.pages.inc
 files[] = node.install
 files[] = node.test
+files[] = node.tokens.inc
 required = TRUE
diff --git a/modules/node/node.tokens.inc b/modules/node/node.tokens.inc
new file mode 100644
index 000000000000..915ac4100876
--- /dev/null
+++ b/modules/node/node.tokens.inc
@@ -0,0 +1,205 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for node-related data.
+ */
+
+
+
+/**
+ * Implement hook_token_info().
+ */
+function node_token_info() {
+  $type = array(
+    'name' => t('Nodes'),
+    'description' => t('Tokens related to individual nodes.'),
+    'needs-data' => 'node',
+  );
+
+  // 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."),
+    'type' => 'date',
+  );
+  $node['changed'] = array(
+    'name' => t("Date changed"),
+    'description' => t("The date the node was most recently updated."),
+    'type' => 'date',
+  );
+  $node['author'] = array(
+    'name' => t("Author"),
+    'description' => t("The author of the node."),
+    'type' => 'user',
+  );
+
+  return array(
+    'types' => array('node' => $type),
+    'tokens' => array('node' => $node),
+  );
+}
+
+/**
+ * Implement hook_tokens().
+ */
+function node_tokens($type, $tokens, array $data = array(), array $options = array()) {
+  $url_options = array('absolute' => TRUE);
+  if (isset($options['language'])) {
+    $url_options['language'] = $language;
+    $language_code = $language->language;
+  }
+  else {
+    $language_code = NULL;
+  }
+  $sanitize = !empty($options['sanitize']);
+
+  $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 ? check_plain($node->name) : $node->name;
+          break;
+
+        case 'title':
+          $replacements[$original] = $sanitize ? check_plain($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':
+          $name = ($node->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $node->name;
+          $replacements[$original] = $sanitize ? filter_xss($name) : $name;
+          break;
+
+        case 'created':
+          $replacements[$original] = format_date($node->created, 'medium', '', NULL, $language_code);
+          break;
+
+        case 'changed':
+          $replacements[$original] = format_date($node->changed, 'medium', '', NULL, $language_code);
+          break;
+      }
+    dsm('node');
+    }
+
+    if ($author_tokens = token_find_with_prefix($tokens, 'author')) {
+      $author = user_load($node->uid);
+      $replacements += token_generate('user', $author_tokens, array('user' => $author), $options);
+    }
+
+    if ($created_tokens = token_find_with_prefix($tokens, 'created')) {
+      $replacements += token_generate('date', $created_tokens, array('date' => $node->created), $options);
+    }
+
+    if ($changed_tokens = token_find_with_prefix($tokens, 'changed')) {
+      $replacements += token_generate('date', $changed_tokens, array('date' => $node->changed), $options);
+    }
+  }
+
+  return $replacements;
+}
diff --git a/modules/poll/poll.info b/modules/poll/poll.info
index d59cc9b93f80..fd2f3149b8e0 100644
--- a/modules/poll/poll.info
+++ b/modules/poll/poll.info
@@ -8,3 +8,4 @@ files[] = poll.module
 files[] = poll.pages.inc
 files[] = poll.install
 files[] = poll.test
+files[] = poll.tokens.inc
diff --git a/modules/poll/poll.tokens.inc b/modules/poll/poll.tokens.inc
new file mode 100644
index 000000000000..9fe6857addd1
--- /dev/null
+++ b/modules/poll/poll.tokens.inc
@@ -0,0 +1,91 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for values specific to Poll nodes.
+ */
+
+/**
+ * Implement hook_token_info().
+ */
+function poll_token_info() {
+  $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."),
+  );
+
+  return array(
+    'tokens' => array('node' => $node),
+  );
+}
+
+/**
+ * Implement hook_tokens().
+ */
+function poll_tokens($type, $tokens, array $data = array(), array $options = array()) {
+  $url_options = array('absolute' => TRUE);
+  $sanitize = !empty($options['sanitize']);
+
+  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, $language_code);
+          break;
+      }
+    }
+  }
+
+  return $replacements;
+}
diff --git a/modules/statistics/statistics.info b/modules/statistics/statistics.info
index a3a509124d5e..83b4351c3c7d 100644
--- a/modules/statistics/statistics.info
+++ b/modules/statistics/statistics.info
@@ -9,3 +9,4 @@ files[] = statistics.admin.inc
 files[] = statistics.pages.inc
 files[] = statistics.install
 files[] = statistics.test
+files[] = statistics.tokens.inc
diff --git a/modules/statistics/statistics.tokens.inc b/modules/statistics/statistics.tokens.inc
new file mode 100644
index 000000000000..2a0e5988846c
--- /dev/null
+++ b/modules/statistics/statistics.tokens.inc
@@ -0,0 +1,63 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for node visitor statistics.
+ */
+
+/**
+ * Implement hook_token_info().
+ */
+function statistics_token_info() {
+  $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."),
+    'type' => 'date',
+  );
+
+  return array(
+    'tokens' => array('node' => $node),
+  );
+}
+
+/**
+ * Implement hook_tokens().
+ */
+function statistics_tokens($type, $tokens, array $data = array(), array $options = array()) {
+  $url_options = array('absolute' => TRUE);
+
+  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 += token_generate('date', $created_tokens, array('date' => $statistics['timestamp']), $options);
+    }
+  }
+
+  return $replacements;
+}
diff --git a/modules/system/system.info b/modules/system/system.info
index 2709050a7b4b..8eb7db900d48 100644
--- a/modules/system/system.info
+++ b/modules/system/system.info
@@ -11,4 +11,5 @@ files[] = image.gd.inc
 files[] = system.install
 files[] = system.test
 files[] = system.tar.inc
+files[] = system.tokens.inc
 required = TRUE
diff --git a/modules/system/system.module b/modules/system/system.module
index 5a63d4a65bcd..14083cabab87 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -2641,7 +2641,7 @@ function system_send_email_action_form($context) {
     '#title' => t('Recipient'),
     '#default_value' => $context['recipient'],
     '#maxlength' => '254',
-    '#description' => t('The email address to which the message should be sent OR enter %author if you would like to send an e-mail to the author of the original post.', array('%author' => '%author')),
+    '#description' => t('The email address to which the message should be sent OR enter [node:author:mail], [comment:author:mail], etc. if you would like to send an e-mail to the author of the original post.'),
   );
   $form['subject'] = array(
     '#type' => 'textfield',
@@ -2656,7 +2656,7 @@ function system_send_email_action_form($context) {
     '#default_value' => $context['message'],
     '#cols' => '80',
     '#rows' => '20',
-    '#description' => t('The message that should be sent. You may include the following variables: %site_name, %username, %node_url, %node_type, %title, %teaser, %body, %term_name, %term_description, %term_id, %vocabulary_name, %vocabulary_description, %vocabulary_id. Not all variables will be available in all contexts.'),
+    '#description' => t('The message that should be sent. You may include placeholders like [node:title], [user:name], and [comment:body] to represent data that will be different each time message is sent. Not all placeholders will be available in all contexts.'),
   );
   return $form;
 }
@@ -2692,59 +2692,14 @@ function system_send_email_action_submit($form, $form_state) {
  * Implement a configurable Drupal action. Sends an email.
  */
 function system_send_email_action($object, $context) {
-  global $user;
-
-  switch ($context['hook']) {
-    case 'node':
-      // Because this is not an action of type 'node' the node
-      // will not be passed as $object, but it will still be available
-      // in $context.
-      $node = $context['node'];
-      break;
-    // The comment hook provides nid, in $context.
-    case 'comment':
-      $comment = $context['comment'];
-      $node = node_load($comment->nid);
-      break;
-    case 'user':
-      // Because this is not an action of type 'user' the user
-      // object is not passed as $object, but it will still be available
-      // in $context.
-      $account = $context['account'];
-      if (isset($context['node'])) {
-        $node = $context['node'];
-      }
-      elseif ($context['recipient'] == '%author') {
-        // If we don't have a node, we don't have a node author.
-        watchdog('error', 'Cannot use %author token in this context.');
-        return;
-      }
-      break;
-    default:
-      // We are being called directly.
-      $node = $object;
+  if (empty($context['node'])) {
+    $context['node'] = $object;
   }
 
-  $recipient = $context['recipient'];
-
-  if (isset($node)) {
-    if (!isset($account)) {
-      $account = user_load($node->uid);
-    }
-    if ($recipient == '%author') {
-      $recipient = $account->mail;
-    }
-  }
-
-  if (!isset($account)) {
-    $account = $user;
-
-  }
+  $recipient = token_replace($context['recipient'], $context);
+  
   $language = user_preferred_language($account);
-  $params = array('account' => $account, 'object' => $object, 'context' => $context);
-  if (isset($node)) {
-    $params['node'] = $node;
-  }
+  $params = array('context' => $context);
 
   if (drupal_mail('system', 'action_send_email', $recipient, $language, $params)) {
     watchdog('action', 'Sent email to %recipient', array('%recipient' => $recipient));
@@ -2758,39 +2713,11 @@ function system_send_email_action($object, $context) {
  * Implement hook_mail().
  */
 function system_mail($key, &$message, $params) {
-  $account = $params['account'];
   $context = $params['context'];
-  $variables = array(
-    '%site_name' => variable_get('site_name', 'Drupal'),
-    '%username' => $account->name,
-  );
-  if ($context['hook'] == 'taxonomy') {
-    $object = $params['object'];
-    $vocabulary = taxonomy_vocabulary_load($object->vid);
-    $variables += array(
-      '%term_name' => $object->name,
-      '%term_description' => $object->description,
-      '%term_id' => $object->tid,
-      '%vocabulary_name' => $vocabulary->name,
-      '%vocabulary_description' => $vocabulary->description,
-      '%vocabulary_id' => $vocabulary->vid,
-    );
-  }
 
-  // Node-based variable translation is only available if we have a node.
-  if (isset($params['node'])) {
-    $node = $params['node'];
-    $variables += array(
-      '%uid' => $node->uid,
-      '%node_url' => url('node/' . $node->nid, array('absolute' => TRUE)),
-      '%node_type' => node_type_get_name($node),
-      '%title' => $node->title,
-      '%teaser' => $node->teaser,
-      '%body' => $node->body,
-    );
-  }
-  $subject = strtr($context['subject'], $variables);
-  $body = strtr($context['message'], $variables);
+  $subject = token_replace($context['subject'], $context);
+  $body = token_replace($context['message'], $context);
+
   $message['subject'] .= str_replace(array("\r", "\n"), '', $subject);
   $message['body'][] = drupal_html_to_text($body);
 }
@@ -2802,7 +2729,7 @@ function system_message_action_form($context) {
     '#default_value' => isset($context['message']) ? $context['message'] : '',
     '#required' => TRUE,
     '#rows' => '8',
-    '#description' => t('The message to be displayed to the current user. You may include the following variables: %site_name, %username, %node_url, %node_type, %title, %teaser, %body, %term_name, %term_description, %term_id, %vocabulary_name, %vocabulary_description, %vocabulary_id. Not all variables will be available in all contexts.'),
+    '#description' => t('The message to be displayed to the current user. You may include placeholders like [node:title], [user:name], and [comment:body] to represent data that will be different each time message is sent. Not all placeholders will be available in all contexts.'),
   );
   return $form;
 }
@@ -2815,56 +2742,11 @@ function system_message_action_submit($form, $form_state) {
  * A configurable Drupal action. Sends a message to the current user's screen.
  */
 function system_message_action(&$object, $context = array()) {
-  global $user;
-  $variables = array(
-    '%site_name' => variable_get('site_name', 'Drupal'),
-    '%username' => $user->name ? $user->name : variable_get('anonymous', t('Anonymous')),
-  );
-
-  // This action can be called in any context, but if placeholders
-  // are used a node object must be present to be the source
-  // of substituted text.
-  switch ($context['hook']) {
-    case 'node':
-      // Because this is not an action of type 'node' the node
-      // will not be passed as $object, but it will still be available
-      // in $context.
-      $node = $context['node'];
-      break;
-    // The comment hook also provides the node, in context.
-    case 'comment':
-      $comment = $context['comment'];
-      $node = node_load($comment->nid);
-      break;
-    case 'taxonomy':
-      $vocabulary = taxonomy_vocabulary_load($object->vid);
-      $variables = array_merge($variables, array(
-        '%term_name' => $object->name,
-        '%term_description' => $object->description,
-        '%term_id' => $object->tid,
-        '%vocabulary_name' => $vocabulary->name,
-        '%vocabulary_description' => $vocabulary->description,
-        '%vocabulary_id' => $vocabulary->vid,
-        )
-      );
-      break;
-    default:
-      // We are being called directly.
-      $node = $object;
-  }
-
-  if (isset($node) && is_object($node)) {
-    $variables = array_merge($variables, array(
-      '%uid' => $node->uid,
-      '%node_url' => url('node/' . $node->nid, array('absolute' => TRUE)),
-      '%node_type' => check_plain(node_type_get_name($node)),
-      '%title' => filter_xss($node->title),
-      '%teaser' => filter_xss($node->teaser),
-      '%body' => filter_xss($node->body),
-      )
-    );
+  if (empty($context['node'])) {
+    $context['node'] = $object;
   }
-  $context['message'] = strtr($context['message'], $variables);
+
+  $context['message'] = token_replace($context['message'], $context);
   drupal_set_message($context['message']);
 }
 
@@ -2889,7 +2771,7 @@ function system_goto_action_submit($form, $form_state) {
 }
 
 function system_goto_action($object, $context) {
-  drupal_goto($context['url']);
+  drupal_goto(token_replace($context['url'], $context));
 }
 
 /**
diff --git a/modules/system/system.test b/modules/system/system.test
index ba33835e25e7..491fbc8fe390 100644
--- a/modules/system/system.test
+++ b/modules/system/system.test
@@ -1058,3 +1058,60 @@ class QueueTestCase extends DrupalWebTestCase {
     return $score;
   }
 }
+
+/**
+ * Test token replacement in strings.
+ */
+class TokenReplaceTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Token replacement',
+      'description' => 'Generates text using placeholders for dummy content to check token replacement.',
+      'group' => '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));
+    $node->title = '<blink>Blinking Text</blink>';
+    global $user;
+
+    $source  = '[node:title]';         // Title of the node we passed in
+    $source .= '[node:author:name]';   // Node author's name
+    $source .= '[node:created:since]'; // Time since the node was created
+    $source .= '[current-user:name]';  // Current user's name
+    $source .= '[user:name]';          // No user passed in, should be untouched
+    $source .= '[date:small]';         // Small date format of REQUEST_TIME
+    $source .= '[bogus:token]';        // Nonexistent token, should be untouched
+
+    $target  = check_plain($node->title);
+    $target .= check_plain($account->name);
+    $target .= format_interval(REQUEST_TIME - $node->created, 2);
+    $target .= check_plain($user->name);
+    $target .= '[user:name]';
+    $target .= format_date(REQUEST_TIME, 'small');
+    $target .= '[bogus:token]';
+
+    $result = token_replace($source, array('node' => $node));
+
+    // Check that the results of token_generate are sanitized properly. This does NOT
+    // test the cleanliness of every token -- just that the $sanitize flag is being
+    // passed properly through the call stack and being handled correctly by a 'known'
+    // token, [node:title].
+    $this->assertFalse(strcmp($target, $result), t('Basic placeholder tokens replaced.'));
+    
+    $raw_tokens = array(
+      'node' => array('title' => '[node:title]'),
+    );
+    $generated = token_generate($raw_tokens, array('node' => $node));
+    $this->assertFalse(strcmp($generated['[node:title]'], check_plain($node->title)), t('Token sanitized.'));
+
+    $generated = token_generate($raw_tokens, array('node' => $node), array('sanitize' => FALSE));
+    $this->assertFalse(strcmp($generated['[node:title]'], $node->title), t('Unsanitized token generated properly.'));
+  }
+}
diff --git a/modules/system/system.tokens.inc b/modules/system/system.tokens.inc
new file mode 100644
index 000000000000..5ab7a573bc89
--- /dev/null
+++ b/modules/system/system.tokens.inc
@@ -0,0 +1,308 @@
+<?php
+// $Id$
+
+/**
+ * @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_token_info().
+ */
+function system_token_info() {
+  $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."),
+    'needs-data' => 'file',
+  );
+
+  // 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"),
+    '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."),
+    'type' => 'date',
+  );
+  $file['node'] = array(
+    'name' => t("Node"),
+    'description' => t("The node the file is attached to."),
+    'type' => 'date',
+  );
+  $file['owner'] = array(
+    'name' => t("Owner"),
+    'description' => t("The user who originally uploaded the file."),
+    'type' => 'user',
+  );
+
+  return array(
+    'types' => $types,
+    'tokens' => array(
+      'site' => $site,
+      'date' => $date,
+      'file' => $file,
+    ),
+  );
+}
+
+/**
+ * Implement hook_tokens().
+ */
+function system_tokens($type, $tokens, array $data = array(), array $options = array()) {
+  $url_options = array('absolute' => TRUE);
+  if (isset($language)) {
+    $url_options['language'] = $language;
+  }
+  $sanitize = !empty($options['sanitize']);
+
+  $replacements = array();
+
+  if ($type == 'site') {
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        case 'name':
+          $site_name = variable_get('site_name', 'Drupal');
+          $replacements[$original] = $sanitize ? check_plain($site_name) : $site_name;
+          break;
+
+        case 'slogan':
+          $slogan = variable_get('site_slogan', '');
+          $replacements[$original] = $sanitize ? check_plain($slogan) : $slogan;
+          break;
+
+        case 'mission':
+          $mission = variable_get('site_mission', '');
+          $replacements[$original] = $sanitize ? filter_xss($mission) : $mission;
+          break;
+
+        case 'mail':
+          $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 += token_generate('node', $node_tokens, array('node' => $node), $language, $sanitize);
+    }
+
+    if ($date_tokens = token_find_with_prefix($tokens, 'timestamp')) {
+      $replacements += token_generate('date', $date_tokens, array('date' => $file->timestamp), $language, $sanitize);
+    }
+
+    if (($owner_tokens = token_find_with_prefix($tokens, 'owner')) && $account = user_load($file->uid)) {
+      $replacements += token_generate('user', $owner_tokens, array('user' => $account), $language, $sanitize);
+    }
+  }
+
+  return $replacements;
+}
diff --git a/modules/taxonomy/taxonomy.info b/modules/taxonomy/taxonomy.info
index 35dfa1480566..0bec863d24cc 100644
--- a/modules/taxonomy/taxonomy.info
+++ b/modules/taxonomy/taxonomy.info
@@ -9,3 +9,4 @@ files[] = taxonomy.admin.inc
 files[] = taxonomy.pages.inc
 files[] = taxonomy.install
 files[] = taxonomy.test
+files[] = taxonomy.tokens.inc
diff --git a/modules/taxonomy/taxonomy.tokens.inc b/modules/taxonomy/taxonomy.tokens.inc
new file mode 100644
index 000000000000..fc321700f802
--- /dev/null
+++ b/modules/taxonomy/taxonomy.tokens.inc
@@ -0,0 +1,189 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for taxonomy terms and vocabularies.
+ */
+
+/**
+ * Implement hook_token_info().
+ */
+function taxonomy_token_info() {
+  $types['term'] = array(
+    'name' => t("Taxonomy terms"),
+    'description' => t("Tokens related to taxonomy terms."),
+    'needs-data' => 'term',
+  );
+  $types['vocabulary'] = array(
+    'name' => t("Vocabularies"),
+    'description' => t("Tokens related to taxonomy vocabularies."),
+    'needs-data' => 'vocabulary',
+  );
+
+  // 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."),
+    'type' => 'vocabulary',
+  );
+  $term['parent'] = array(
+    'name' => t("Parent term"),
+    'description' => t("The parent term of the taxonomy term, if one exists."),
+    'type' => 'term',
+  );
+
+  return array(
+    'types' => $types,
+    'tokens' => array(
+      'term' => $term,
+      'vocabulary' => $vocabulary,
+    ),
+  );
+}
+
+/**
+ * Implement hook_tokens().
+ */
+function taxonomy_tokens($type, $tokens, array $data = array(), array $options = array()) {
+  $replacements = array();
+  $sanitize = !empty($options['sanitize']);
+
+  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 ? check_plain($term->name) : $term->name;
+          break;
+
+        case 'description':
+          $replacements[$original] = $sanitize ? filter_xss($term->description) : $term->description;
+          break;
+
+        case 'url':
+          $replacements[$original] = url(taxonomy_term_path($term), 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] = check_plain($vocabulary->name);
+          break;
+
+        case 'parent':
+          $parents = taxonomy_get_parents($term->tid);
+          $parent = array_pop($parents);
+          $replacements[$original] = check_plain($parent->name);
+          break;
+      }
+    }
+
+    if ($vocabulary_tokens = token_find_with_prefix($tokens, 'vocabulary')) {
+      $vocabulary = taxonomy_vocabulary_load($term->vid);
+      $replacements += token_generate('vocabulary', $vocabulary_tokens, array('vocabulary' => $vocabulary), $options);
+    }
+
+    if ($vocabulary_tokens = token_find_with_prefix($tokens, 'parent')) {
+      $parents = taxonomy_get_parents($term->tid);
+      $parent = array_pop($parents);
+      $replacements += token_generate('term', $vocabulary_tokens, array('term' => $parent), $options);
+    }
+  }
+
+  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 ? check_plain($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;
+}
diff --git a/modules/upload/upload.info b/modules/upload/upload.info
index d5039f5214a0..a3253433c39d 100644
--- a/modules/upload/upload.info
+++ b/modules/upload/upload.info
@@ -8,3 +8,4 @@ files[] = upload.module
 files[] = upload.admin.inc
 files[] = upload.install
 files[] = upload.test
+files[] = upload.tokens.inc
diff --git a/modules/upload/upload.tokens.inc b/modules/upload/upload.tokens.inc
new file mode 100644
index 000000000000..92c18367a927
--- /dev/null
+++ b/modules/upload/upload.tokens.inc
@@ -0,0 +1,45 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for uploaded files attached to nodes.
+ */
+
+/**
+ * Implement hook_token_info().
+ */
+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.'),
+    'type' => 'file',
+    )
+  );
+  return $results;
+}
+
+/**
+ * Implement hook_tokens().
+ */
+function upload_tokens($type, $tokens, array $data = array(), array $options = array()) {
+  $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 += token_generate('file', $upload_tokens, array('file' => $upload), $options);
+    }
+  }
+
+  return $replacements;
+}
diff --git a/modules/user/user.info b/modules/user/user.info
index d4c24fa77220..cd916a8733df 100644
--- a/modules/user/user.info
+++ b/modules/user/user.info
@@ -9,4 +9,5 @@ files[] = user.admin.inc
 files[] = user.pages.inc
 files[] = user.install
 files[] = user.test
+files[] = user.tokens.inc
 required = TRUE
diff --git a/modules/user/user.tokens.inc b/modules/user/user.tokens.inc
new file mode 100644
index 000000000000..11048e72c0cd
--- /dev/null
+++ b/modules/user/user.tokens.inc
@@ -0,0 +1,129 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for user-related data.
+ */
+
+/**
+ * Implement hook_token_info().
+ */
+function user_token_info() {
+  $types['user'] = array(
+    'name' => t('Users'),
+    'description' => t('Tokens related to individual user accounts.'),
+    'needs-data' => 'user',
+  );
+  $types['current-user'] = array(
+    'name' => t('Current user'),
+    'description' => t('Tokens related to the currently logged in user.'),
+    'type' => 'user',
+  );
+
+  $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['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['last-login'] = array(
+    'name' => t("Last login"),
+    'description' => t("The date the user last logged in to the site."),
+    'type' => 'date',
+  );
+  $user['created'] = array(
+    'name' => t("Created"),
+    'description' => t("The date the user account was created."),
+    'type' => 'date',
+  );
+
+  return array(
+    'types' => array('user' => $types),
+    'tokens' => array('user' => $user),
+  );
+}
+
+/**
+ * Implement hook_tokens().
+ */
+function user_tokens($type, $tokens, array $data = array(), array $options = array()) {
+  global $user;
+  $url_options = array('absolute' => TRUE);
+  if (isset($options['language'])) {
+    $url_options['language'] = $language;
+    $language_code = $language->language;
+  }
+  else {
+    $language_code = NULL;
+  }
+  $sanitize = !empty($options['sanitize']);
+
+  $replacements = array();
+
+  if ($type == 'user' && !empty($data['user'])) {
+    $account = $data['user'];
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        // Basic user account information.
+        case 'uid':
+          $replacements[$original] = $account->uid;
+          break;
+
+        case 'name':
+          $name = ($account->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $account->name;
+          $replacements[$original] = $sanitize ? filter_xss($name) : $name;
+          break;
+
+        case 'mail':
+          $replacements[$original] = $sanitize ? check_plain($account->mail) : $account->mail;
+          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;
+
+        // These tokens are default variations on the chained tokens handled below.
+        case 'last-login':
+          $replacements[$original] = format_date($account->login, 'medium', '', NULL, $language_code);
+          break;
+
+        case 'created':
+          $replacements[$original] = format_date($account->created, 'medium', '', NULL, $language_code);
+          break;
+      }
+    }
+
+    if ($login_tokens = token_find_with_prefix($tokens, 'last-login')) {
+      $replacements += token_generate('date', $login_tokens, array('date' => $account->login), $options);
+    }
+
+    if ($registered_tokens = token_find_with_prefix($tokens, 'created')) {
+      $replacements += token_generate('date', $registered_tokens, array('date' => $account->created), $options);
+    }
+  }
+  if ($type == 'current-user') {
+    global $user;
+    $replacements += token_generate('user', $tokens, array('user' => $user), $options);
+  }
+
+  return $replacements;
+}
-- 
GitLab