Steps to Reproduce:
1. Add an article
2. Set Input filter to Full HTML
3. Add

<script>
alert('I am broken'); 
</script>

4. Save the node. No alert :(

What's happening?
The "Correct faulty HTML" filter, a.k.a. po' man's tidy does this (basically):

$dom = DomDocument::loadHTML($content);
foreach (elements in the $content)
  $content .= $dom->saveXML($element);

The purpose is to run the HTML through the DOM classes in PHP which will format the output, close tags, etc.

The problem

When the DOM Parser reads

<script>
alert('I am broken'); 
</script>

It transforms it into:

+ TextElement - the script tag
--DOMCdataSection (the contents of the script tag).

When outputting with saveHTML(), the functions corollary, this outputs the script tag just as we input it. But alas, saveHTML doesn't allow you to export fragments. So the original author of this code decided to iterate through the nodes and use saveXML(). saveXML() is fine, except it will output strict XML, which means CDATA around the script content.

HTML 4 browsers don't like this, so script never executes.

#158992: Inline JavaScript is XHTML invalid

addressed this issue for inline script added via drupal_add_js() by throwing a comment hack around the CDATA.

Possible solutions

1. The attached patch.
This fixes the problem by catching elements which might have this problem, and applying the same (or similar) comments to the issue mentioned above. It works, and is worth committing so that people can use JS widgets in blocks, etc. It's not pretty, but I think it is the only way to do it and maintain HTML 5 compat.

2. Different approach.
Since Tidy is bundled with PHP 5, perhaps it makes sense to just make that filter dependent on tidy. That is really the proper way to do this.

Comments

JacobSingh’s picture

Status: Active » Needs review

Status: Needs review » Needs work

The last submitted patch, filter_dom_script_cdata.patch, failed testing.

JacobSingh’s picture

StatusFileSize
new1.81 KB

Sorry, last patch was not what I intended

JacobSingh’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, filter_dom_script_cdata.patch, failed testing.

JacobSingh’s picture

Status: Needs work » Needs review
StatusFileSize
new1.73 KB

Okay, once more. Removed something which was potentially extra (but we might just want because safeXML() can collapse script tags I think... let's see.

robloach’s picture

Without patch:

Hello! <script type="text/javascript">alert('hi');</script> Goodbye

No alert.

Hello! <script type="text/javascript">
alert('hi');
</script> Goodbye

No alert.

With patch:

Hello! <script type="text/javascript">alert('hi');</script> Goodbye

No alert.

Hello! <script type="text/javascript">
alert('hi');
</script> Goodbye

No alert.

Console log
..... I'm mostly getting this error in Firebug:

syntax error <![CDATA[\n

This in Chrome:

Uncaught SyntaxError: Unexpected token <

Note that it works fine with just a normal <script> tag.

JacobSingh’s picture

Did you clear the filter cache after applying the patch? And /or resubmit the node?

JacobSingh’s picture

Confirmed with Rob, needed to re-submit the node.

Please review!

damien tournoud’s picture

Status: Needs review » Needs work

If I read that correctly, it will only work on first level CDATA elements. We probably need/want to dive into the children.

Tidy is bundled with PHP5, but not enabled by default, which makes it unsuitable as a replacement.

JacobSingh’s picture

@Damien
Why would we have multi-level elements in a script tag?

Anyway, the way we are doing it is really hacky. It's probably preferable to use saveHTML() and regex the stuff inside the body.

-J

JacobSingh’s picture

Ah, yes... if the content is

Something

alert('damn')

It won't work...

Okay... honestly, we should open up the Tidy debate. Perhaps this is a pre-req. This is seriously not what Dom API is indented for...

Recursing way deep down into every element and saveXML() on it? Doesn't seem pretty to me. This is a fairly large problem.

I'm going to try the saveHTML() route and see what happens...

JacobSingh’s picture

Thinking aloud..

I guess we could xpath out the script tags and go at them... that's not pretty or fast, but doable in a filter cache I suppose.

JacobSingh’s picture

Status: Needs work » Needs review
StatusFileSize
new3.48 KB

okay, re-rolled.

Also added tests. As a side note, we should consider merging this with the CDATA related code in drupal_get_js(). But I'd recommend splitting that off to get this critical bug fixed.

damien tournoud’s picture

That looks pretty much ok to me.

gábor hojtsy’s picture

Why is any filtering run on a full HTML format anyway?

robloach’s picture

Status: Needs review » Reviewed & tested by the community

Works with both <script> and <script type="text/javascript"> now. Nicely done, Jacob.

Although "Full HTML" makes it seem like no filter is run on it, it seems sane to cleans the HTML and make sure it doesn't break the rest of the page. If we are to remove the feature, it should be in a seperate issue.

dries’s picture

Patch review:

+++ modules/filter/filter.module
@@ -819,6 +819,15 @@ function filter_dom_load($text) {
 function filter_dom_serialize($dom_document) {

@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+ * Adds comments around <!CDATA section in a dom element

- This seems to be missing the word 'a'. That or 'section' should be 'sections' (plural).

- We should explain why/when this function is to be used.

+++ modules/filter/filter.module
@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+ * DocDocument::loadHTML makes CDATA sections from the contents of inline script
+ * and style tags.  This can cause HTML 4 browsers to throw exceptions.

DocDocument::loadHTML is related to this how? If this is a public API I think we should provide more context. Right now, the relation to DocDocument::loadHTML is unclear. I recommend we write: 'DocDocument::loadHTML in filter_dom_load()'.

+++ modules/filter/filter.module
@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+  foreach($dom_element->childNodes as $node) {

Missing space after 'foreach'.

+++ modules/filter/filter.module
@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+      // @see drupal_get_js()

Please be more specific.

+++ modules/filter/filter.module
@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+      $frag = $dom_document->createDocumentFragment();

We don't abbreviate variables. Please write $fragment.

Powered by Dreditor.

dries’s picture

Reviewing this patch, I agree with Jacob that using saveXML() to clean-up broken HTML is a hack. To be honest, the proposed patch introduces a WTF but is necessary evil if we stick with saveXML(). I would be OK switching to Tidy, even if that is not always available. I have not investigated it deeply, but it will probably do a better and faster job.

I pinged sun, the filter system maintainer, to chime in.

(Off-topic: it seems like Tidy would also be useful for validating Drupal's output in SimpleTest. See tidy_error_count() for details.)

sun’s picture

Status: Reviewed & tested by the community » Needs review

Looks good. I don't think that Tidy is an option - cheap/shared hosts may not have it, but HTMLCorrector filter should produce the same results on all Drupal sites. Bug reports would be insane to debug and test otherwise.

In addition to Dries' review:

+++ modules/filter/filter.module
@@ -819,6 +819,15 @@ function filter_dom_load($text) {
+  

@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+ * 

Trailing white-space here.

+++ modules/filter/filter.module
@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+ * DocDocument::loadHTML makes CDATA sections from the contents of inline script

DOMDocument::loadHTML

+++ modules/filter/filter.module
@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+ * @param $dom_document
+ * @param $dom_element

Missing @param description.

+++ modules/filter/filter.module
@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+ * @param $comment_start
+ *   String to use as a comment start marker to escape the CDATA declaration.
+ * @param $comment_end
+ *   String to use as a comment end marker to escape the CDATA declaration.

We should add a note about "Defaults to inline JS..." here.

+++ modules/filter/filter.module
@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+ * @return void

Please remove @return entirely.

+++ modules/filter/filter.module
@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+function filter_dom_serialize_fix_cdata_element($dom_document, $dom_element, $comment_start = '//', $comment_end = '') {

Could we replace "fix" with "escape" or similar?

+++ modules/filter/filter.module
@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+      // @see drupal_get_js()

Can we move this into the PHPDocblock of the function, please?

+++ modules/filter/filter.module
@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+      $embed_prefix = "\n<!--{$comment_start}--><![CDATA[{$comment_start} ><!--{$comment_end}\n";
+      $embed_suffix = "\n{$comment_start}--><!]]>{$comment_end}\n";

Actually it seems we don't do this yet for CSS, but it looks ok to do it here. Did you verify that this CDATA comment trickery really works the same for inline CSS?

The CDATA tag in drupal_get_js() doesn't use a space after $comment_start - would be good to remove it here as well.

+++ modules/filter/filter.test
@@ -1065,6 +1065,27 @@ class FilterUnitTestCase extends DrupalUnitTestCase {
+    $f = _filter_htmlcorrector('<script type="text/javascript">alert("test")</script>');

We should add at least 3 more tests:

1) SCRIPT without any attribute.

2) SCRIPT nested a bit deeper, e.g. DIV > P > A > SCRIPT

3) SCRIPT/STYLE with other nodes (text and/or inline) before and/or after it.

Powered by Dreditor.

JacobSingh’s picture

StatusFileSize
new3.61 KB

Okay I think I got the nits out. Thanks

On the larger issue:
I think Dries and I are referring to not providing this filter at all if the user doesn't have Tidy. Or perhaps, providing this hacky implementation as a contrib mod.

There are also PHP based tidiers, but of course, they are a bit slow which is likely a problem in this case.

Exceptions to nits:

+++ modules/filter/filter.module
@@ -826,6 +835,38 @@ function filter_dom_serialize($dom_document) {
+      // @see drupal_get_js()

Can we move this into the PHPDocblock of the function, please?

It's too bad we don't use PHPDockblocks or any standard PHP documentation. If we did we'd have to specify return type and input types. Would make Drupal a lot more pleasant to work w/ in my IDE.
But at any rate, it is perfectly valid to use @see inline. I think it makes more sense here, because the duplicate code is right there, not at the top of the function.

"The @see tag may be used to document any element (global variable, include, page, class, function, define, method, variable)"
- http://manual.phpdoc.org/HTMLSmartyConverter/HandS/phpDocumentor/tutoria...

More tests.

I don't see how those tests would be testing anything new, but feel free to add them if you feel called.

Patch attached.

Status: Needs review » Needs work

The last submitted patch, filter_dom_script_cdata.patch, failed testing.

JacobSingh’s picture

Status: Needs work » Needs review

Oh, another option:

I haven't tested this much, but this will work too and is a lot simpler / faster:

$d = DOMDocument::loadHTML('<html><body><script>...</script></body></html>');
print $d->saveHTML();
preg_match('/<body>(.*)<\/body>/s', $d->saveHTML(), $reg);
print $reg[1];

It isn't HTML 5 compatible of course. And adding <!DOCTYPE html> to the input string doesn't help :(

(btw, not adding the <body and <html to the input string causes <script to go in head!)

Best,
Jacob

JacobSingh’s picture

StatusFileSize
new3.61 KB

Ah, I remember why I added that space :)

//> looks like a closing tag to the DOM Parser (which is is). That's why you need a space there.

dries’s picture

I chatted with sun a bit and as the maintainer of some WYSIWYG modules, he feels it is an important filter. I'm not really in-depth with Drupal's WYSIWYG modules but apparently there are more support requests when this filter is not enabled/available. Based on that recommendation, I suggest we create a new issue: "Use PHP's Tidy component for the clean-up HTML filter feature" targetted for D8.

dries’s picture

Status: Needs review » Needs work

I committed #24 as it fixes a critical bug. Let's create a follow-up issue and mark this fixed. Settings to 'needs work' until that issue is created. Thanks Jacob.

JacobSingh’s picture

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

pwolanin’s picture

Status: Closed (fixed) » Needs work

I am still having this fail for anything other than trivial scripts. I don't think it's fixed.

The only way I can get it to work is removing the htmltidy from the filter chain. For example, try putting the twitter widget in a block: http://twitter.com/goodies/widget_profile

pwolanin’s picture

Status: Needs work » Active
damien tournoud’s picture

Status: Active » Needs review
StatusFileSize
new1.37 KB

Without a proper doctype, the serialization to XML doesn't know that it cannot fold empty tags for some historical HTML tags (such as <script>).

sun’s picture

Looks good.

As far as I can see, we're repeating this in various places (and we most probably also repeat that mistake all over again). Would it make sense to introduce a helper function? And perhaps even a variable for the doctype string, so users can eventually tweak this if required?

Status: Needs review » Needs work

The last submitted patch, 721536-doctype-htmlcorrector.patch, failed testing.

JacobSingh’s picture

Tests need fixing... Might also help to include the case which was failing for Peter in the tests?

pwolanin’s picture

The fails in the test result:

Test name	Pass	Fail	Exception
CollapsedCore filters (FilterUnitTestCase) [Filter]	127	2	0
Message	Group	Filename	Line	Function	Status
HTML corrector -- tag closing.	Other	filter.test	1000	FilterUnitTestCase->testHtmlCorrectorFilter()	
HTML corrector -- Let proper XHTML pass thru.	Other	filter.test	1025	FilterUnitTestCase->testHtmlCorrectorFilter()	


CollapsedText summary (TextSummaryTestCase) [Field types]	106	10	0
Message	Group	Filename	Line	Function	Status
Generated summary "<p></p>" matches expected "<p />".	Other	text.test	363	TextSummaryTestCase->callTextSummary()	
Generated summary "<p></p>" matches expected "<p />".	Other	text.test	363	TextSummaryTestCase->callTextSummary()	
Generated summary "<p></p>" matches expected "<p />".	Other	text.test	363	TextSummaryTestCase->callTextSummary()	
Generated summary "<p></p>" matches expected "<p />".	Other	text.test	363	TextSummaryTestCase->callTextSummary()	
Generated summary "<p></p>" matches expected "<p />".	Other	text.test	363	TextSummaryTestCase->callTextSummary()	
Generated summary "<p></p>" matches expected "<p />".	Other	text.test	363	TextSummaryTestCase->callTextSummary()	
Generated summary "<p></p>" matches expected "<p />".	Other	text.test	363	TextSummaryTestCase->callTextSummary()	
Generated summary "<p></p>" matches expected "<p />".	Other	text.test	363	TextSummaryTestCase->callTextSummary()	
Generated summary "<p></p>" matches expected "<p />".	Other	text.test	363	TextSummaryTestCase->callTextSummary()	
Generated summary "<p></p>" matches expected "<p />".	Other	text.test	363	TextSummaryTestCase->callTextSummary()	
jpmckinney’s picture

Status: Needs review » Needs work
StatusFileSize
new1.72 KB

<p> is not a self-closing tag: refer to the XHTML DTD http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd

I've fixed the tests to not expect broken XHTML.

jpmckinney’s picture

Status: Needs work » Needs review
jpmckinney’s picture

Status: Needs work » Needs review
StatusFileSize
new2.43 KB

Sorry, guys, but <span> is not a self-closing tag either, in the XHTML DTD (Drupal serves up XHTML+RDFa 1.0, which is still XHTML).

Status: Needs review » Needs work

The last submitted patch, 721536-37.patch, failed testing.

jpmckinney’s picture

Status: Needs work » Needs review
StatusFileSize
new3.01 KB

Aha, found one last incorrect self-closing p tag.

With respect to the test with the self-closing span tag, I removed that test altogether, as it was testing for "Let proper XHTML pass thru", and as I do not know the reason for that test's inclusion, I could not come up with a suitable substitute for the test string "<span class="test" />". In general, there are too many possible proper XHTML strings, so we should not be testing for "Let proper XHTML pass thru" unless there was a bug that would not let it through and we are testing for regressions.

David_Rothstein’s picture

Title: Inline Javascript does not work due to unescaped CDATA element created by » HTML corrector filter has problems with unescaped CDATA and incorrectly closed tags
StatusFileSize
new5.35 KB

Ugh, I filed a duplicate issue at #766332: HTML corrector filter self-closes tags that should not be self-closed and worked on it for a while there - totally missed this one since the issue title was out of date :(

The approach here seems to be a lot better than the one I was working on; however, my patch has some more comprehensive tests that I think do a better job checking that this works. So, this patch takes the tests from my duplicate issue and combines them with the approach here.

JacobSingh’s picture

Why aren't we using LIBXML_NOEMPTYTAG in DomDocument::saveXML() ?

David_Rothstein’s picture

As far as I understand we don't want that because it will convert e.g. even <br /> to <br></br>, but for those kinds of tags a single closed tag is preferred.

Look at the patch - you'll see I added a test to specifically make sure we don't let that happen :)

See also:
http://www.w3.org/TR/xhtml1/#C_2

sun’s picture

Status: Needs review » Reviewed & tested by the community

Thanks, looks good.

dries’s picture

Agreed. Committed to CVS HEAD. Thanks!

aspilicious’s picture

If this is committed, does that means this is fixed?

aspilicious’s picture

Status: Reviewed & tested by the community » Fixed

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.