WICKET-4937 Add IResponseFilter that can filter out invalid XML characters
Project: http://git-wip-us.apache.org/repos/asf/wicket/repo Commit: http://git-wip-us.apache.org/repos/asf/wicket/commit/5599f619 Tree: http://git-wip-us.apache.org/repos/asf/wicket/tree/5599f619 Diff: http://git-wip-us.apache.org/repos/asf/wicket/diff/5599f619 Branch: refs/heads/wicket-1.5.x Commit: 5599f6199b9620d0dcda322e23d704e383721c5e Parents: 02f9f08 Author: Martin Tzvetanov Grigorov <[email protected]> Authored: Thu Dec 20 10:31:53 2012 +0200 Committer: Martin Tzvetanov Grigorov <[email protected]> Committed: Thu Dec 20 10:33:00 2012 +0200 ---------------------------------------------------------------------- .../response/filter/XmlCleaningResponseFilter.java | 119 +++++++++++++++ .../filter/XmlCleaningResponseFilterTest.java | 90 +++++++++++ 2 files changed, 209 insertions(+), 0 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/wicket/blob/5599f619/wicket-core/src/main/java/org/apache/wicket/response/filter/XmlCleaningResponseFilter.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/main/java/org/apache/wicket/response/filter/XmlCleaningResponseFilter.java b/wicket-core/src/main/java/org/apache/wicket/response/filter/XmlCleaningResponseFilter.java new file mode 100644 index 0000000..e55621d --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/response/filter/XmlCleaningResponseFilter.java @@ -0,0 +1,119 @@ +package org.apache.wicket.response.filter; + +import org.apache.wicket.util.string.AppendingStringBuffer; + +/** + * An IResponseFilter that removes all invalid XML characters. + * By default it is used only for Wicket <em>Ajax</em> responses. + * + * <p>If the application needs to use it for other use cases then it can either override + * {@linkplain #shouldFilter(AppendingStringBuffer)} in the case it is used as IResponseFilter or + * {@linkplain #stripNonValidXMLCharacters(AppendingStringBuffer)} can be used directly. + * </p> + * + * <p>Usage: + * + * MyApplication.java + * <code><pre> + * public void init() { + * super.init(); + * + * getRequestCycleSettings().addResponseFilter(new XmlCleaningResponseFilter()); + * } + * </pre></code> + * </p> + */ +public class XmlCleaningResponseFilter implements IResponseFilter +{ + @Override + public AppendingStringBuffer filter(AppendingStringBuffer responseBuffer) + { + AppendingStringBuffer result = responseBuffer; + if (shouldFilter(responseBuffer)) + { + result = stripNonValidXMLCharacters(responseBuffer); + } + return result; + } + + /** + * Decides whether the filter should be applied. + * + * @param responseBuffer The buffer to filter + * @return {@code true} if the buffer brings Ajax response + */ + protected boolean shouldFilter(AppendingStringBuffer responseBuffer) + { + // To avoid reading the whole buffer for non-Ajax responses + // read just the first N chars. A candidate can start with: + // <?xml version="1.0" encoding="UTF-8" standalone="yes"?><ajax-response> + int min = Math.min(150, responseBuffer.length()); + String firstNChars = responseBuffer.substring(0, min); + return firstNChars.contains("<ajax-response>"); + } + + /** + * This method ensures that the output String has only + * valid XML unicode characters as specified by the + * XML 1.0 standard. For reference, please see + * <a href="http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char">the + * standard</a>. This method will return an empty + * String if the input is null or empty. + * + * @param input The StringBuffer whose non-valid characters we want to remove. + * @return The in String, stripped of non-valid characters. + */ + public AppendingStringBuffer stripNonValidXMLCharacters(AppendingStringBuffer input) + { + char[] chars = input.getValue(); + AppendingStringBuffer out = null; + + int codePoint; + + int i = 0; + + while (i < input.length()) + { + codePoint = Character.codePointAt(chars, i, chars.length); + + if (!isValidXmlChar(codePoint)) + { + if (out == null) + { + out = new AppendingStringBuffer(chars.length); + out.append(input.subSequence(0, i)); + } + else + { + out.append(Character.toChars(codePoint)); + } + } + else if (out != null) + { + out.append(Character.toChars(codePoint)); + } + + // Increment with the number of code units(java chars) needed to represent a Unicode char. + i += Character.charCount(codePoint); + } + + return out != null ? out : input; + } + + /** + * Checks whether the character represented by this codePoint is + * a valid in XML documents. + * + * @param codePoint The codePoint for the checked character + * @return {@code true} if the character can be used in XML documents + */ + protected boolean isValidXmlChar(int codePoint) + { + return (codePoint == 0x9) || + (codePoint == 0xA) || + (codePoint == 0xD) || + ((codePoint >= 0x20) && (codePoint <= 0xD7FF)) || + ((codePoint >= 0xE000) && (codePoint <= 0xFFFD)) || + ((codePoint >= 0x10000) && (codePoint <= 0x10FFFF)); + } +} http://git-wip-us.apache.org/repos/asf/wicket/blob/5599f619/wicket-core/src/test/java/org/apache/wicket/response/filter/XmlCleaningResponseFilterTest.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/test/java/org/apache/wicket/response/filter/XmlCleaningResponseFilterTest.java b/wicket-core/src/test/java/org/apache/wicket/response/filter/XmlCleaningResponseFilterTest.java new file mode 100644 index 0000000..fbba553 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/response/filter/XmlCleaningResponseFilterTest.java @@ -0,0 +1,90 @@ +package org.apache.wicket.response.filter; + +import org.apache.wicket.util.string.AppendingStringBuffer; +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for XmlCleaningResponseFilter + */ +public class XmlCleaningResponseFilterTest extends Assert { + + public static final String AJAX_RESPONSE_START = "<ajax-response>b"; + public static final String AJAX_RESPONSE_END = "b</ajax-response>"; + + /** + * Tests that invalid XML characters are removed + * @throws Exception + */ + @Test + public void filterInvalid() throws Exception + { + XmlCleaningResponseFilter filter = new XmlCleaningResponseFilter(); + int[] invalidChars = new int[] {0x0008, 0x0010, 0xD800, 0xDDDD, 0xFFFE}; + + for (int invalidChar : invalidChars) + { + CharSequence text = createText(invalidChar); + + AppendingStringBuffer filtered = filter.filter(new AppendingStringBuffer(text)); + assertEquals(String.format("checking Unicode codepoint 0x%X:", invalidChar), AJAX_RESPONSE_START+AJAX_RESPONSE_END, filtered.toString()); + } + } + + /** + * Tests that valid XML characters are preserved + * @throws Exception + */ + @Test + public void filterValid() throws Exception + { + XmlCleaningResponseFilter filter = new XmlCleaningResponseFilter(); + int[] validChars = new int[] {0x9, 0xA, 'a', 0xE000, 0xFFFC, 0x10400}; + + for (int validChar : validChars) + { + CharSequence text = createText(validChar); + + AppendingStringBuffer filtered = filter.filter(new AppendingStringBuffer(text)); + assertEquals(String.format("checking Unicode codepoint 0x%X:", validChar), text.toString(), filtered.toString()); + } + } + + // using a int because a Java char cannot represent all Unicode characters; some require two chars. + private CharSequence createText(int ch) + { + String character = new String(new int[] {ch}, 0, 1); + return new StringBuilder() + .append(AJAX_RESPONSE_START) + .append(character) + .append(AJAX_RESPONSE_END); + } + + /** + * Asserts that XmlCleaningResponseFilter#shouldFilter() returns true when + * there is <ajax-response> in the text to filter + * @throws Exception + */ + @Test + public void shouldFilter() throws Exception + { + XmlFilter filter = new XmlFilter(); + + assertFalse(filter.shouldFilter(new AppendingStringBuffer("anything"))); + + assertTrue(filter.shouldFilter( + new AppendingStringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"" + + " standalone=\"yes\"><ajax-response></ajax-response>"))); + } + + /** + * Makes #shouldFilter() method public + */ + private static class XmlFilter extends XmlCleaningResponseFilter + { + @Override + public boolean shouldFilter(AppendingStringBuffer responseBuffer) { + return super.shouldFilter(responseBuffer); + } + } +}
