diff --git a/modules/block/block.module b/modules/block/block.module
index e0cb691..90bc6a1 100644
--- a/modules/block/block.module
+++ b/modules/block/block.module
@@ -242,7 +242,7 @@ function block_block_save($delta = 0, $edit = array()) {
function block_block_view($delta = '') {
$block = db_query('SELECT body, format FROM {block_custom} WHERE bid = :bid', array(':bid' => $delta))->fetchObject();
$data['subject'] = NULL;
- $data['content'] = check_markup($block->body, $block->format, '', TRUE);
+ $data['content'] = check_markup($block->body, $block->format, array('cache' => TRUE, 'type' => 'block', 'object' => $block));
return $data;
}
diff --git a/modules/book/book.test b/modules/book/book.test
index cc61778..4cbc6f4 100644
--- a/modules/book/book.test
+++ b/modules/book/book.test
@@ -165,7 +165,7 @@ class BookTestCase extends DrupalWebTestCase {
// Check printer friendly version.
$this->drupalGet('book/export/html/' . $node->nid);
$this->assertText($node->title, t('Printer friendly title found.'));
- $this->assertRaw(check_markup($node->body[LANGUAGE_NONE][0]['value'], $node->body[LANGUAGE_NONE][0]['format']), t('Printer friendly body found.'));
+ $this->assertRaw(check_markup($node->body[LANGUAGE_NONE][0]['value'], $node->body[LANGUAGE_NONE][0]['format'], array('type' => 'node', 'object' => $node)), t('Printer friendly body found.'));
$number++;
}
@@ -234,7 +234,7 @@ class BookTestCase extends DrupalWebTestCase {
// Make sure each part of the book is there.
foreach ($nodes as $node) {
$this->assertText($node->title, t('Node title found in printer friendly version.'));
- $this->assertRaw(check_markup($node->body[LANGUAGE_NONE][0]['value'], $node->body[LANGUAGE_NONE][0]['format']), t('Node body found in printer friendly version.'));
+ $this->assertRaw(check_markup($node->body[LANGUAGE_NONE][0]['value'], $node->body[LANGUAGE_NONE][0]['format'], array('type' => 'node', 'object' => $node)), t('Node body found in printer friendly version.'));
}
// Make sure we can't export an unsupported format.
diff --git a/modules/comment/comment.module b/modules/comment/comment.module
index 60a9ca4..fb2c7f8 100644
--- a/modules/comment/comment.module
+++ b/modules/comment/comment.module
@@ -2158,7 +2158,7 @@ function comment_submit($comment) {
// 1) Filter it into HTML
// 2) Strip out all HTML tags
// 3) Convert entities back to plain-text.
- $comment->subject = truncate_utf8(trim(decode_entities(strip_tags(check_markup($comment->comment_body[LANGUAGE_NONE][0]['value'], $comment->comment_body[LANGUAGE_NONE][0]['format'])))), 29, TRUE);
+ $comment->subject = truncate_utf8(trim(decode_entities(strip_tags(check_markup($comment->comment_body[LANGUAGE_NONE][0]['value'], $comment->comment_body[LANGUAGE_NONE][0]['format'], array('type' => 'comment', 'object' => $comment))))), 29, TRUE);
// Edge cases where the comment body is populated only by HTML tags will
// require a default subject.
if ($comment->subject == '') {
diff --git a/modules/comment/comment.test b/modules/comment/comment.test
index 770e01d..88fd2b5 100644
--- a/modules/comment/comment.test
+++ b/modules/comment/comment.test
@@ -1701,7 +1701,7 @@ class CommentTokenReplaceTestCase extends CommentHelperCase {
$tests['[comment:mail]'] = check_plain($this->admin_user->mail);
$tests['[comment:homepage]'] = check_url($comment->homepage);
$tests['[comment:title]'] = filter_xss($comment->subject);
- $tests['[comment:body]'] = _text_sanitize($instance, LANGUAGE_NONE, $comment->comment_body[LANGUAGE_NONE][0], 'value');
+ $tests['[comment:body]'] = _text_sanitize($instance, LANGUAGE_NONE, 'comment', $comment, $comment->comment_body[LANGUAGE_NONE][0], 'value');
$tests['[comment:url]'] = url('comment/' . $comment->cid, $url_options + array('fragment' => 'comment-' . $comment->cid));
$tests['[comment:edit-url]'] = url('comment/' . $comment->cid . '/edit', $url_options);
$tests['[comment:created:since]'] = format_interval(REQUEST_TIME - $comment->created, 2, $language->language);
diff --git a/modules/comment/comment.tokens.inc b/modules/comment/comment.tokens.inc
index d62b7e2..2194d7c 100644
--- a/modules/comment/comment.tokens.inc
+++ b/modules/comment/comment.tokens.inc
@@ -157,7 +157,7 @@ function comment_tokens($type, $tokens, array $data = array(), array $options =
case 'body':
$item = $comment->comment_body[LANGUAGE_NONE][0];
$instance = field_info_instance('comment', 'body', 'comment_body');
- $replacements[$original] = $sanitize ? _text_sanitize($instance, LANGUAGE_NONE, $item, 'value') : $item['value'];
+ $replacements[$original] = $sanitize ? _text_sanitize($instance, LANGUAGE_NONE, 'comment', $comment, $item, 'value') : $item['value'];
break;
// Comment related URLs.
diff --git a/modules/field/modules/text/text.module b/modules/field/modules/text/text.module
index 89c605c..1fe8f99 100644
--- a/modules/field/modules/text/text.module
+++ b/modules/field/modules/text/text.module
@@ -156,9 +156,9 @@ function text_field_load($entity_type, $entities, $field, $instances, $langcode,
// Only process items with a cacheable format, the rest will be handled
// by formatters if needed.
if (empty($instances[$id]['settings']['text_processing']) || filter_format_allowcache($item['format'])) {
- $items[$id][$delta]['safe_value'] = isset($item['value']) ? _text_sanitize($instances[$id], $langcode, $item, 'value') : '';
+ $items[$id][$delta]['safe_value'] = isset($item['value']) ? _text_sanitize($instances[$id], $langcode, $entity_type, $entity, $item, 'value') : '';
if ($field['type'] == 'text_with_summary') {
- $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? _text_sanitize($instances[$id], $langcode, $item, 'summary') : '';
+ $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? _text_sanitize($instances[$id], $langcode, $entity_type, $entity, $item, 'summary') : '';
}
}
}
@@ -261,7 +261,7 @@ function text_field_formatter_view($entity_type, $entity, $field, $instance, $la
case 'text_default':
case 'text_trimmed':
foreach ($items as $delta => $item) {
- $output = _text_sanitize($instance, $langcode, $item, 'value');
+ $output = _text_sanitize($instance, $langcode, $entity_type, $entity, $item, 'value');
if ($display['type'] == 'text_trimmed') {
$output = text_summary($output, $instance['settings']['text_processing'] ? $item['format'] : NULL, $display['settings']['trim_length']);
}
@@ -272,10 +272,10 @@ function text_field_formatter_view($entity_type, $entity, $field, $instance, $la
case 'text_summary_or_trimmed':
foreach ($items as $delta => $item) {
if (!empty($item['summary'])) {
- $output = _text_sanitize($instance, $langcode, $item, 'summary');
+ $output = _text_sanitize($instance, $langcode, $entity_type, $entity, $item, 'summary');
}
else {
- $output = _text_sanitize($instance, $langcode, $item, 'value');
+ $output = _text_sanitize($instance, $langcode, $entity_type, $entity, $item, 'value');
$output = text_summary($output, $instance['settings']['text_processing'] ? $item['format'] : NULL, $display['settings']['trim_length']);
}
$element[$delta] = array('#markup' => $output);
@@ -301,7 +301,11 @@ function text_field_formatter_view($entity_type, $entity, $field, $instance, $la
* @param $instance
* The instance definition.
* @param $langcode
- * The language associated to $item.
+ * The language associated to $item.
+ * @param $entity_type
+ * The entity type to sanitize, used for contextual data.
+ * @param $entity
+ * The entity to sanitize, used for contextual data.
* @param $item
* The field value to sanitize.
* @param $column
@@ -310,13 +314,13 @@ function text_field_formatter_view($entity_type, $entity, $field, $instance, $la
* @return
* The sanitized string.
*/
-function _text_sanitize($instance, $langcode, $item, $column) {
+function _text_sanitize($instance, $langcode, $entity_type, $entity, $item, $column) {
// If the value uses a cacheable text format, text_field_load() precomputes
// the sanitized string.
if (isset($item["safe_$column"])) {
return $item["safe_$column"];
}
- return $instance['settings']['text_processing'] ? check_markup($item[$column], $item['format'], $langcode) : check_plain($item[$column]);
+ return $instance['settings']['text_processing'] ? check_markup($item[$column], $item['format'], array('langcode' => $langcode, 'type' => $entity_type, 'object' => $entity)) : check_plain($item[$column]);
}
/**
diff --git a/modules/filter/filter.api.php b/modules/filter/filter.api.php
index 6675e4a..13fb27a 100644
--- a/modules/filter/filter.api.php
+++ b/modules/filter/filter.api.php
@@ -65,6 +65,11 @@
* details.
* - process callback: (required) The name the function that performs the
* actual filtering. See hook_filter_FILTER_process() for details.
+ * - 'context callback': The name of a function that extracts contextual data
+ * from the object of the filtering. For example, a filter that uses the
+ * name of the author of a node would use this to provide the name of the
+ * author as context to the filter. This function will provide additional
+ * contextual data to the prepare and process callbacks.
* - cache (default TRUE): Specifies whether the filtered text can be cached.
* Note that setting this to FALSE makes the entire text format not
* cacheable, which may have an impact on the site's overall performance.
@@ -189,8 +194,9 @@ function hook_filter_FILTER_settings($form, &$form_state, $filter, $format, $def
* The filter object containing settings for the given format.
* @param $format
* The text format object assigned to the text to be filtered.
- * @param $langcode
- * The language code of the text to be filtered.
+ * @param $context
+ * The contextual data for the text to be filtered, including the
+ * language code and any context provided in a 'context callback'.
* @param $cache
* A Boolean indicating whether the filtered text is going to be cached in
* {cache_filter}.
@@ -200,7 +206,7 @@ function hook_filter_FILTER_settings($form, &$form_state, $filter, $format, $def
* @return
* The prepared, escaped text.
*/
-function hook_filter_FILTER_prepare($text, $filter, $format, $langcode, $cache, $cache_id) {
+function hook_filter_FILTER_prepare($text, $filter, $format, $context, $cache, $cache_id) {
// Escape and tags.
$text = preg_replace('|(.+?)|se', "[codefilter_code]$1[/codefilter_code]", $text);
return $text;
@@ -222,8 +228,9 @@ function hook_filter_FILTER_prepare($text, $filter, $format, $langcode, $cache,
* The filter object containing settings for the given format.
* @param $format
* The text format object assigned to the text to be filtered.
- * @param $langcode
- * The language code of the text to be filtered.
+ * @param $context
+ * The contextual data for the text to be filtered, including the
+ * language code and any context provided in a 'context callback'.
* @param $cache
* A Boolean indicating whether the filtered text is going to be cached in
* {cache_filter}.
@@ -233,13 +240,52 @@ function hook_filter_FILTER_prepare($text, $filter, $format, $langcode, $cache,
* @return
* The filtered text.
*/
-function hook_filter_FILTER_process($text, $filter, $format, $langcode, $cache, $cache_id) {
+function hook_filter_FILTER_process($text, $filter, $format, $context, $cache, $cache_id) {
$text = preg_replace('|\[codefilter_code\](.+?)\[/codefilter_code\]|se', "
$1", $text); return $text; } /** + * Context callback for hook_filter_info(). + * + * Note: This is not really a hook. The function name is manually specified via + * 'context callback' in hook_filter_info(), with this recommended callback + * name pattern. It is called from check_markup(). + * + * See hook_filter_info() for a description of the filtering process. This step + * is where the contextual data is generated for the filter. + * + * @param $text + * The text string to be filtered. + * @param $filter + * The filter object containing settings for the given format. + * @param $format + * The text format object assigned to the text to be filtered. + * @param $type + * The type of object being filtered. + * @param $object + * The object variable of the object being filtered. + * + * @return + * An associative array of contextual data from the object that, upon + * changing, will require the filter to be rerun. + */ +function hook_filter_FILTER_context($text, $filter, $format, $type, $object) { + switch ($type) { + case 'node': + case 'comment': + $account = user_load($object->uid); + // Filter will have to recache when the account name changes, either when + // the author of the comment/node changes or when the user changes their + // name. + return array( + 'user_name' => $account->name, + ); + } +} + +/** * Tips callback for hook_filter_info(). * * Note: This is not really a hook. The function name is manually specified via diff --git a/modules/filter/filter.module b/modules/filter/filter.module index 773fa80..32dae4c 100644 --- a/modules/filter/filter.module +++ b/modules/filter/filter.module @@ -704,18 +704,31 @@ function filter_list_format($format_id) { * @param $format_id * The format id of the text to be filtered. If no format is assigned, the * fallback format will be used. - * @param $langcode - * Optional: the language code of the text to be filtered, e.g. 'en' for - * English. This allows filters to be language aware so language specific - * text replacement can be implemented. - * @param $cache - * Boolean whether to cache the filtered output in the {cache_filter} table. - * The caller may set this to FALSE when the output is already cached - * elsewhere to avoid duplicate cache lookups and storage. + * @param $options + * An associative array of options, used to override the defaults. Possible + * values include: + * - cache: Boolean whether to cache the filtered output in the + * {cache_filter} table. The caller may set this to TRUE when the output + * is not being cached elsewhere. Otherwise, this should be left as FALSE + * to avoid duplicate cache lookups and storage. Defaults to FALSE. + * - langcode: the language code of the text to be filtered, e.g. 'en' for + * English. This allows filters to be language aware so language specific + * text replacement can be implemented. Defaults to ''. + * - object: an object from which contextual data can be pulled, such as a + * node object, a user object, or a block. Defaults to NULL for no context. + * - type: a string that identifies the type of object, such as 'node', or + * 'user', or 'block'. Defaults to '' for no context. * * @ingroup sanitization */ -function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) { +function check_markup($text, $format_id = NULL, $options = array()) { + $options += array( + 'cache' => FALSE, + 'langcode' => '', + 'object' => NULL, + 'type' => '', + ); + if (!isset($format_id)) { $format_id = filter_fallback_format(); } @@ -726,10 +739,28 @@ function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) } // Check for a cached version of this piece of text. - $cache = $cache && !empty($format->cache); + $cache = $options['cache'] && !empty($format->cache); $cache_id = ''; + + // Get a complete list of filters, ordered properly. + $filters = filter_list_format($format->format); + $filter_info = filter_get_filters(); + + $context = array( + 'langcode' => $options['langcode'], + ); + foreach ($filters as $name => $filter) { + if ($filter->status && isset($filter_info[$name]['context callback']) && function_exists($filter_info[$name]['context callback'])) { + $function = $filter_info[$name]['context callback']; + $result = $function($text, $filter, $format, $options['type'], $options['object']); + if (is_array($result)) { + $context += $result; + } + } + } + if ($cache) { - $cache_id = $format->format . ':' . $langcode . ':' . hash('sha256', $text); + $cache_id = $format->format . ':' . hash('sha256', $text) . ':' . hash('sha256', serialize($context)); if ($cached = cache_get($cache_id, 'cache_filter')) { return $cached->data; } @@ -739,15 +770,11 @@ function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) // need to deal with one possibility. $text = str_replace(array("\r\n", "\r"), "\n", $text); - // Get a complete list of filters, ordered properly. - $filters = filter_list_format($format->format); - $filter_info = filter_get_filters(); - // Give filters the chance to escape HTML-like data such as code or formulas. foreach ($filters as $name => $filter) { if ($filter->status && isset($filter_info[$name]['prepare callback']) && function_exists($filter_info[$name]['prepare callback'])) { $function = $filter_info[$name]['prepare callback']; - $text = $function($text, $filter, $format, $langcode, $cache, $cache_id); + $text = $function($text, $filter, $format, $context, $cache, $cache_id); } } @@ -755,12 +782,14 @@ function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) foreach ($filters as $name => $filter) { if ($filter->status && isset($filter_info[$name]['process callback']) && function_exists($filter_info[$name]['process callback'])) { $function = $filter_info[$name]['process callback']; - $text = $function($text, $filter, $format, $langcode, $cache, $cache_id); + $text = $function($text, $filter, $format, $context, $cache, $cache_id); } } // Store in cache with a minimum expiration time of 1 day. if ($cache) { + // Avoid flooding the cache_filter table with outdated entries. + cache_clear_all($format->format . ':' . hash('sha256', $text) . ':', 'cache_filter', TRUE); cache_set($cache_id, $text, 'cache_filter', REQUEST_TIME + (60 * 60 * 24)); } diff --git a/modules/filter/filter.test b/modules/filter/filter.test index a3d1bde..584d4e8 100644 --- a/modules/filter/filter.test +++ b/modules/filter/filter.test @@ -1750,3 +1750,80 @@ class FilterHooksTestCase extends DrupalWebTestCase { } } +/** + * Tests the filter system's contextual data handling. + */ +class FilterContextTestCase extends DrupalWebTestCase { + protected $format_id; + + public static function getInfo() { + return array( + 'name' => 'Filter context', + 'description' => 'Tests the ability of the filtering system to incorporate textual data.', + 'group' => 'Filter', + ); + } + + function setUp() { + parent::setUp('filter_test_2'); + $admin_user = $this->drupalCreateUser(array('administer filters')); + $this->drupalLogin($admin_user); + $format_name = $this->randomName(); + $this->drupalPost('admin/config/content/formats/add', array('name' => $format_name, 'format' => strtolower($format_name), 'filters[filter_test_2_context][status]' => TRUE), t('Save configuration')); + $this->drupalLogout(); + $this->format_id = db_query("SELECT format FROM {filter_format} WHERE name = :name", array(':name' => $format_name))->fetchField(); + filter_formats_reset(); + } + + /** + * Test to make sure that filters are able to use contextual data, and that + * the caching system works properly with contextual data changes. + */ + function testFilterContext() { + $text1 = $this->randomName(); + $text2 = $this->randomName(); + $var1 = (object)array('one' => $this->randomName()); + $var2 = (object)array('one' => $this->randomName()); + + // Text 1 with object 1. + $result1 = check_markup($text1, $this->format_id, array('cache' => TRUE, 'type' => 'test', 'object' => $var1)); + list($success, $text, $var, $rand1) = explode('/', $result1); + $this->assertEqual($success, 'Success', 'Context successfully passed.'); + $this->assertEqual($text, $text1, 'Text matched.'); + $this->assertEqual($var, $var1->one, 'Contextual variable matched.'); + $rand_store[] = $rand1; + + // Text 1 with object 1 again - make sure random variable stays the same. + $result2 = check_markup($text1, $this->format_id, array('cache' => TRUE, 'type' => 'test', 'object' => $var1)); + list($success, $text, $var, $rand2) = explode('/', $result2); + $this->assertEqual($success, 'Success', 'Context successfully passed.'); + $this->assertEqual($text, $text1, 'Text matched.'); + $this->assertEqual($var, $var1->one, 'Contextual variable matched.'); + $this->assertEqual($rand1, $rand2, 'Filter with contextual data was successfully cached.'); + + // Text 1 with object 2 - make sure cache invalidates. + $result3 = check_markup($text1, $this->format_id, array('cache' => TRUE, 'type' => 'test', 'object' => $var2)); + list($success, $text, $var, $rand3) = explode('/', $result3); + $this->assertEqual($success, 'Success', 'Context successfully passed.'); + $this->assertEqual($text, $text1, 'Text matched.'); + $this->assertEqual($var, $var2->one, 'Contextual variable matched.'); + $this->assertNotEqual($rand1, $rand3, 'Filter with contextual data successfully invalidated cache.'); + + // Back to text 1 with object 1 - cache should still be valid. + $result4 = check_markup($text1, $this->format_id, array('cache' => TRUE, 'type' => 'test', 'object' => $var1)); + list($success, $text, $var, $rand4) = explode('/', $result4); + $this->assertEqual($success, 'Success', 'Context successfully passed.'); + $this->assertEqual($text, $text1, 'Text matched.'); + $this->assertEqual($var, $var1->one, 'Contextual variable matched.'); + $this->assertEqual($rand1, $rand4, 'Filter with contextual data did not have cache invalidated.'); + + // Text 2 with object 2 - should be entirely new data. + $result5 = check_markup($text2, $this->format_id, array('cache' => TRUE, 'type' => 'test', 'object' => $var2)); + list($success, $text, $var, $rand5) = explode('/', $result5); + $this->assertEqual($success, 'Success', 'Context successfully passed.'); + $this->assertEqual($text, $text2, 'Text matched.'); + $this->assertEqual($var, $var2->one, 'Contextual variable matched.'); + $this->assertNotEqual($rand1, $rand5, 'No cache mix-up.'); + $this->assertNotEqual($rand3, $rand5, 'No cache mix-up.'); + } +} diff --git a/modules/node/node.api.php b/modules/node/node.api.php index 3e8029c..fa86a1f 100644 --- a/modules/node/node.api.php +++ b/modules/node/node.api.php @@ -709,7 +709,7 @@ function hook_node_update_index($node) { $text = ''; $comments = db_query('SELECT subject, comment, format FROM {comment} WHERE nid = :nid AND status = :status', array(':nid' => $node->nid, ':status' => COMMENT_PUBLISHED)); foreach ($comments as $comment) { - $text .= '