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.
| Comment | File | Size | Author |
|---|---|---|---|
| #41 | filter-html-corrector-721536-41.patch | 5.35 KB | David_Rothstein |
| #40 | 721536-40.patch | 3.01 KB | jpmckinney |
| #38 | 721536-37.patch | 2.43 KB | jpmckinney |
| #36 | 721536-36.patch | 1.72 KB | jpmckinney |
| #31 | 721536-doctype-htmlcorrector.patch | 1.37 KB | damien tournoud |
Comments
Comment #1
JacobSingh commentedComment #3
JacobSingh commentedSorry, last patch was not what I intended
Comment #4
JacobSingh commentedComment #6
JacobSingh commentedOkay, once more. Removed something which was potentially extra (but we might just want because safeXML() can collapse script tags I think... let's see.
Comment #7
robloachWithout patch:
No alert.
No alert.
With patch:
No alert.
No alert.
Console log
..... I'm mostly getting this error in Firebug:
This in Chrome:
Note that it works fine with just a normal <script> tag.
Comment #8
JacobSingh commentedDid you clear the filter cache after applying the patch? And /or resubmit the node?
Comment #9
JacobSingh commentedConfirmed with Rob, needed to re-submit the node.
Please review!
Comment #10
damien tournoud commentedIf 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.
Comment #11
JacobSingh commented@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
Comment #12
JacobSingh commentedAh, 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...
Comment #13
JacobSingh commentedThinking 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.
Comment #14
JacobSingh commentedokay, 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.
Comment #15
damien tournoud commentedThat looks pretty much ok to me.
Comment #16
gábor hojtsyWhy is any filtering run on a full HTML format anyway?
Comment #17
robloachWorks 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.
Comment #18
dries commentedPatch review:
- 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.
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()'.
Missing space after 'foreach'.
Please be more specific.
We don't abbreviate variables. Please write $fragment.
Powered by Dreditor.
Comment #19
dries commentedReviewing 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.)
Comment #20
sunLooks 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:
Trailing white-space here.
DOMDocument::loadHTML
Missing @param description.
We should add a note about "Defaults to inline JS..." here.
Please remove @return entirely.
Could we replace "fix" with "escape" or similar?
Can we move this into the PHPDocblock of the function, please?
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.
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.
Comment #21
JacobSingh commentedOkay 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:
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...
I don't see how those tests would be testing anything new, but feel free to add them if you feel called.
Patch attached.
Comment #23
JacobSingh commentedOh, another option:
I haven't tested this much, but this will work too and is a lot simpler / faster:
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
Comment #24
JacobSingh commentedAh, 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.
Comment #25
dries commentedI 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.
Comment #26
dries commentedI 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.
Comment #27
JacobSingh commented#725260: Use PHP's Tidy component for the clean-up HTML filter
Comment #29
pwolanin commentedI 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
Comment #30
pwolanin commentedComment #31
damien tournoud commentedWithout a proper doctype, the serialization to XML doesn't know that it cannot fold empty tags for some historical HTML tags (such as
<script>).Comment #32
sunLooks 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?
Comment #34
JacobSingh commentedTests need fixing... Might also help to include the case which was failing for Peter in the tests?
Comment #35
pwolanin commentedThe fails in the test result:
Comment #36
jpmckinney commented<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.
Comment #37
jpmckinney commentedComment #38
jpmckinney commentedSorry, 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).
Comment #40
jpmckinney commentedAha, 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.
Comment #41
David_Rothstein commentedUgh, 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.
Comment #42
JacobSingh commentedWhy aren't we using LIBXML_NOEMPTYTAG in DomDocument::saveXML() ?
Comment #43
David_Rothstein commentedAs 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
Comment #44
sunThanks, looks good.
Comment #45
dries commentedAgreed. Committed to CVS HEAD. Thanks!
Comment #46
aspilicious commentedIf this is committed, does that means this is fixed?
Comment #47
aspilicious commentedhttp://drupal.org/cvs?commit=352802