summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDries Buytaert <dries@buytaert.net>2009-08-19 20:19:37 +0000
committerDries Buytaert <dries@buytaert.net>2009-08-19 20:19:37 +0000
commit40003c8307ca64da0cd1ab0ee4d87f4812be8088 (patch)
tree05aa427a035a991cc82b68e42d5eb2282273bac4
parente998857eb8a5ef4d2ebe381a57c19b1b355fe4ef (diff)
downloadbrdo-40003c8307ca64da0cd1ab0ee4d87f4812be8088.tar.gz
brdo-40003c8307ca64da0cd1ab0ee4d87f4812be8088.tar.bz2
- Patch #113614 by eaton, fago, et al: add centralized token/placeholder subsituation to core.
-rw-r--r--includes/common.inc1
-rw-r--r--includes/token.inc238
-rw-r--r--modules/comment/comment.info1
-rw-r--r--modules/comment/comment.tokens.inc248
-rw-r--r--modules/node/node.info1
-rw-r--r--modules/node/node.tokens.inc205
-rw-r--r--modules/poll/poll.info1
-rw-r--r--modules/poll/poll.tokens.inc91
-rw-r--r--modules/statistics/statistics.info1
-rw-r--r--modules/statistics/statistics.tokens.inc63
-rw-r--r--modules/system/system.info1
-rw-r--r--modules/system/system.module150
-rw-r--r--modules/system/system.test57
-rw-r--r--modules/system/system.tokens.inc308
-rw-r--r--modules/taxonomy/taxonomy.info1
-rw-r--r--modules/taxonomy/taxonomy.tokens.inc189
-rw-r--r--modules/upload/upload.info1
-rw-r--r--modules/upload/upload.tokens.inc45
-rw-r--r--modules/user/user.info1
-rw-r--r--modules/user/user.tokens.inc129
20 files changed, 1598 insertions, 134 deletions
diff --git a/includes/common.inc b/includes/common.inc
index f6e64cf47..4e701de9e 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 000000000..7f786121d
--- /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 71a62e3ed..5b6a48acf 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 000000000..09b89116b
--- /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 2f62a9901..300213411 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 000000000..915ac4100
--- /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 d59cc9b93..fd2f3149b 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 000000000..9fe6857ad
--- /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 a3a509124..83b4351c3 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 000000000..2a0e59888
--- /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 2709050a7..8eb7db900 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 5a63d4a65..14083caba 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 ba33835e2..491fbc8fe 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 000000000..5ab7a573b
--- /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 35dfa1480..0bec863d2 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 000000000..fc321700f
--- /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 d5039f521..a3253433c 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 000000000..92c18367a
--- /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 d4c24fa77..cd916a873 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 000000000..11048e72c
--- /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;
+}