From 0c0f97ba80fa756f2143454d9cc9f261f6df1ad0 Mon Sep 17 00:00:00 2001 From: ezyang Date: Sun, 15 Aug 2010 23:13:24 +0000 Subject: [PATCH] Fix #819728: Making use of the HTMLPurifier option Filter.ExtractStyleBlocks (CSSTidy) Thanks Vector- for contributing the patch. --- CHANGELOG.txt | 2 + htmlpurifier.module | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3fdd1db..be9f8bc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ 6.x-2.4, unknown release date - Fixed #839490; can't find library from install profile +- Fixed #819728; make use of Filter.ExtractStyleBlocks. Thanks + Vector- for contributing the patch. 6.x-2.3, released 2010-06-09 - Fixed #819914; version never updates diff --git a/htmlpurifier.module b/htmlpurifier.module index 93d443f..a363d83 100644 --- a/htmlpurifier.module +++ b/htmlpurifier.module @@ -1,5 +1,5 @@ content['teaser']['#value'], $node->nid ); + } + else { + _htmlpurifier_add_css( $node->content['body']['#value'], $node->nid ); + } + } + // @todo: Deal with CCK fields - probably needs to go in op alter? +} + +/** + * Helper function for hook_nodeapi + * Finds extracted style blocks based on a cache link left by hook_filter + * Aggregates the extracted style blocks and adds them to the document head + * Also removes the cache link left in hook_filter to the CSS cache + * + * @param string &$field + * Field to process, this should be the actual field value + * ex. $node->content['body']['#value'] + * + * @param int $nid + * Node ID of the node to which these stylesheets belong + * Since filters don't know their node context, we have to use a token + * to generate the stylesheet scope, and replace it in hook_nodeapi + */ +function _htmlpurifier_add_css( &$field, $nid ) { + + // Some basic validation to assure we really got a rendered field + if (!is_string($field)) { + return; + } + + $cache_matches = array(); + $cache_match = preg_match('##', $field, $cache_matches); + + // If there's an HTML Purifier Cache #, we need to load CSSTidy blocks + if ($cache_match == 1) { + $cid = 'css:' . $cache_matches[1]; + $old = cache_get($cid, 'cache_htmlpurifier'); + + // We should always have some cached style blocks to load, but if we don't, just bail + if ($old) { + $styles = array(); + $style_rendered = ''; + foreach($old->data as $i => $style) { + + // Replace Node ID tokens if necessary, otherwise use cached CSSTidy blocks + // NOTE: This token is forgeable, but we expect that if the user + // is able to invoke this transformation, it will be relatively + // harmless. + if (strpos($style, '[%HTMLPURIFIER:NID%]') !== FALSE) { + $styles[$i] = str_replace('[%HTMLPURIFIER:NID%]', (int) $nid, $style); + } + else { + $styles[$i] = $style; + } + + // Save any CSSTidy blocks we find to be rendered in the document head + if (!empty($style)) { + $style_rendered .= $styles[$i] . "\n"; + } + } + + // Add the rendered stylesheet to the document header + if ($style_rendered != '') { + drupal_set_html_head(''); + } + + // Remove the HTML Purifier cache key from the field argument + $field = str_replace($cache_matches[0], '', $field); + + // If we had to update CSSTidy blocks, cache the results + if ($old->data != $styles) { + cache_set($cid, $styles, 'cache_htmlpurifier', CACHE_PERMANENT); + } + } + } +} + // -- INTERNAL FUNCTIONS ---------------------------------------------------- // @@ -121,8 +207,30 @@ function _htmlpurifier_process($text, $format, $cache = TRUE) { _htmlpurifier_load(); $config = _htmlpurifier_get_config($format); + + // If ExtractStyleBlocks is enabled, we'll need to do a bit more for CSSTidy + $config_extractstyleblocks = $config->get('Filter.ExtractStyleBlocks'); + + // Maybe this works if CSSTidy is at root? CSSTidy could be other places though + if ($config_extractstyleblocks == true) { + _htmlpurifier_load_csstidy(); + } + $purifier = new HTMLPurifier($config); $ret = $purifier->purify($text); + + // If using Filter.ExtractStyleBlocks we need to handle the CSSTidy output + if ($config_extractstyleblocks == true) { + + // We're only going to bother if we're caching! - no caching? no style blocks! + if ($cache) { + + // Get style blocks, cache them, and help hook_nodeapi find the cache + $styles = $purifier->context->get('StyleBlocks'); + cache_set('css:' . $cid, $styles, 'cache_htmlpurifier', CACHE_PERMANENT); + $ret = '' . $ret; + } + } if ($cache) cache_set($cid, $ret, 'cache_htmlpurifier', CACHE_PERMANENT); @@ -195,6 +303,12 @@ function _htmlpurifier_get_config($format) { $config_function($config); } else { $config_data = variable_get("htmlpurifier_config_$format", FALSE); + if (!empty($config_data['Filter.ExtractStyleBlocks'])) { + if (!_htmlpurifier_load_csstidy()) { + $config_data['Filter.ExtractStyleBlocks'] = '0'; + drupal_set_message("Could not enable ExtractStyleBlocks because CSSTidy was not installed. You can download CSSTidy module from http://drupal.org/project/csstidy", 'error', FALSE); + } + } // {FALSE, TRUE, FALSE} = {no index, everything is allowed, don't do mq fix} $config->mergeArrayFromForm($config_data, FALSE, TRUE, FALSE); } @@ -203,6 +317,26 @@ function _htmlpurifier_get_config($format) { } +function _htmlpurifier_load_csstidY() { + // If CSSTidy module is installed, it should have a copy we can use + $csstidy_path = drupal_get_path('module', 'csstidy') .'/csstidy'; + + // Some future-proofing for library path + if (function_exists('libraries_get_path')) { + $csstidy_library = libraries_get_path('csstidy'); + if (file_exists("$csstidy_library/class.csstidy.php")) { + $csstidy_path = $csstidy_library; + } + } + + // Load CSSTidy if we can find it + if (file_exists("$csstidy_path/class.csstidy.php")) { + require_once "$csstidy_path/class.csstidy.php"; + return TRUE; + } + return FALSE; +} + /** * Returns the name of the configuration function for $format, or FALSE if none * exists. Function name will be htmlpurifier_config_N. @@ -246,11 +380,16 @@ function _htmlpurifier_settings($delta, $format) { drupal_add_js(HTMLPurifier_Printer_ConfigForm::getJavaScript(), 'inline'); $form = array(); + $form['dashboard'] = array( '#type' => 'fieldset', '#title' => t('HTML Purifier Dashboard'), '#collapsible' => true, ); + $form['dashboard']["enter_hack"] = array( + // hack to make normal form submission when is pressed + '#value' => '', + ); $form['dashboard']["htmlpurifier_clear_cache"] = array( '#type' => 'submit', '#value' => t('Clear cache (Warning: Can result in performance degradation)'), @@ -334,6 +473,16 @@ function _htmlpurifier_config_hack($form_element, &$form_state) { if (!empty($form_element['#post']) && isset($form_element['#post'][$key])) { $form_state['values'][$key] = $form_element['#post'][$key]; } + foreach ($form_state['values'] as $i => $config_data) { + if (!is_array($config_data)) continue; + if (!empty($config_data['Filter.ExtractStyleBlocks'])) { + if (!empty($config_data['Null_Filter.ExtractStyleBlocks.Scope'])) { + drupal_set_message("You have not set Filter.ExtractStyleBlocks.Scope; this means that users can add CSS that affects all of your Drupal theme and not just their content block. It is recommended to set this to #node-[%HTMLPURIFIER:NID%] (including brackets) which will automatically ensure that CSS directives only apply to their node.", 'warning', FALSE); + } elseif (!isset($config_data['Filter.ExtractStyleBlocks.Scope']) || $config_data['Filter.ExtractStyleBlocks.Scope'] !== '#node-[%HTMLPURIFIER:NID%]') { + drupal_set_message("You have enabled Filter.ExtractStyleBlocks.Scope, but you did not set it to #node-[%HTMLPURIFIER:NID%]; CSS may not work unless you have special theme support.", 'warning', FALSE); + } + } + } return $form_element; } -- 2.11.4.GIT