Commit 40003c83 authored by Dries's avatar Dries

- Patch #113614 by eaton, fago, et al: add centralized token/placeholder subsituation to core.

parent e998857e
......@@ -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');
......
<?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;
}
......@@ -10,3 +10,4 @@ files[] = comment.admin.inc
files[] = comment.pages.inc
files[] = comment.install
files[] = comment.test
files[] = comment.tokens.inc
<?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;
}
......@@ -10,4 +10,5 @@ files[] = node.admin.inc
files[] = node.pages.inc
files[] = node.install
files[] = node.test
files[] = node.tokens.inc
required = TRUE
<?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;