token patch From: fago --- includes/common.inc | 1 includes/token.inc | 183 ++++++++++++++++++++++++++++++++ modules/comment/comment.info | 1 modules/poll/poll.info | 1 modules/statistics/statistics.info | 1 modules/system/system.info | 1 modules/system/system.module | 205 ++++++++++++------------------------ modules/system/system.test | 59 ++++++++++ modules/taxonomy/taxonomy.info | 1 modules/upload/upload.info | 1 10 files changed, 320 insertions(+), 134 deletions(-) create mode 100644 includes/token.inc diff --git a/includes/common.inc b/includes/common.inc index ba9b5bd..bb026b8 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -3549,6 +3549,7 @@ function _drupal_bootstrap_full() { require_once DRUPAL_ROOT . '/includes/form.inc'; require_once DRUPAL_ROOT . '/includes/mail.inc'; require_once DRUPAL_ROOT . '/includes/actions.inc'; + require_once DRUPAL_ROOT . '/includes/token.inc'; // Set the Drupal custom error handler. set_error_handler('_drupal_error_handler'); set_exception_handler('_drupal_exception_handler'); diff --git a/includes/token.inc b/includes/token.inc new file mode 100644 index 0000000..2c63a7a --- /dev/null +++ b/includes/token.inc @@ -0,0 +1,183 @@ + TRUE); + // Add in the current date and the global user by default. + $data += array('currentDate' => REQUEST_TIME, 'currentUser' => $GLOBALS['user']); + $types += array('currentDate' => 'date', 'currentUser' => 'user'); + + foreach ($raw_tokens as $key => $tokens) { + $type = isset($types[$key]) ? $types[$key] : $key; + if (isset($data[$key]) && ($wrapper = drupal_get_property_wrapper($type, $data[$key], $options))) { + foreach ($tokens as $token => $original) { + try { + $results[$original] = _token_get_replacement($wrapper, $token); + } + catch (DrupalEntityPropertyException $e) { + // A token has not been found, so ignore it. + } + } + } + } + return $results; +} + +/** + * Applies chained tokens by getting properties of the given wrapper. + */ +function _token_get_replacement(DrupalPropertyWrapperInterface $wrapper, $token) { + foreach (explode(':', $token) as $i => $name) { + $wrapper = $wrapper->$name; + } + // If no format was given apply the default by converting it to string. + return (string)$wrapper; +} + +/** + * Returns metadata describing token formats. + * + * @param $type + * The type, e.g. date, for which the info shall be returned, or NULL + * to return an array with info about all types. + * + * @see hook_token_format_info() + * @see hook_token_format_info_alter() + */ +function token_get_format_info($type = NULL) { + $data = &drupal_static(__FUNCTION__); + if (!isset($data)) { + if ($cache = cache_get('token_format_info')) { + $data = $cache->data; + } + else { + $data = module_invoke_all('token_format_info'); + drupal_alter('token_format_info', $data); + cache_set('token_format_info', $data); + } + } + if (!empty($type)) { + return isset($data[$type]) ? $data[$type] : array(); + } + return $data; +} diff --git a/modules/system/system.module b/modules/system/system.module index 7047591..b89629d 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -2628,7 +2628,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', @@ -2643,7 +2643,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; } @@ -2679,59 +2679,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; - } - - $recipient = $context['recipient']; - - if (isset($node)) { - if (!isset($account)) { - $account = user_load($node->uid); - } - if ($recipient == '%author') { - $recipient = $account->mail; - } + if (empty($context['node'])) { + $context['node'] = $object; } - 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)); @@ -2745,39 +2700,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); } @@ -2789,7 +2716,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; } @@ -2802,56 +2729,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']); } @@ -2876,7 +2758,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)); } /** @@ -3004,6 +2886,61 @@ function system_image_toolkits() { } /** + * Implementation of hook_token_format_info(). + */ +function system_token_format_info() { + // Date related formats. + $date['small'] = array( + 'name' => t("Small format"), + 'description' => t("A date in 'small' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'small'))), + 'callback' => 'system_format_date', + ); + $date['medium'] = array( + 'name' => t("Medium format"), + 'description' => t("A date in 'medium' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'medium'))), + 'callback' => 'system_format_date', + ); + $date['large'] = array( + 'name' => t("Large format"), + 'description' => t("A date in 'large' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'large'))), + 'callback' => 'system_format_date', + ); + $date['since'] = array( + 'name' => t("Time-since"), + 'description' => t("A data in 'time-since' format. (%date)", array('%date' => format_interval(REQUEST_TIME - 360, 2))), + 'callback' => 'system_format_date', + ); + $date['raw'] = array( + 'name' => t("Raw timestamp"), + 'description' => t("A date in UNIX timestamp format (%date)", array('%date' => REQUEST_TIME)), + 'callback' => 'system_format_date', + ); + return array('date' => array('default format' => 'medium', 'formats' => $date)); +} + +/** + * Callback for formating date tokens. + * @see system_token_format_info(). + */ +function system_format_date($date, array $options, $name) { + $langcode = isset($options['language']) ? $options['language']->language : NULL; + + switch ($name) { + case 'raw': + return filter_xss($date); + + case 'small': + case 'medium': + case 'large': + return format_date($date, $name, '', NULL, $langcode); + + case 'since': + return format_interval((REQUEST_TIME - $date), 2, $langcode); + } +} + + +/** * Attempts to get a file using drupal_http_request and to store it locally. * * @param $url diff --git a/modules/system/system.test b/modules/system/system.test index 1aa1e86..9310d83 100644 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -1058,3 +1058,62 @@ 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 = 'Blinking Text'; + 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 .= '[node:changed]'; // Last update time using the default format. + $source .= '[currentUser:name]'; // Current user's name + $source .= '[user:name]'; // No user passed in, should be untouched + $source .= '[currentDate: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 .= format_date($node->changed, 'medium'); + $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.')); + } +}