From 262080c318194c674c3528f1f1b0b8c33331f9d0 Mon Sep 17 00:00:00 2001 From: Trever Shick Date: Fri, 11 Nov 2016 23:52:07 -0500 Subject: [PATCH] Refactor formatters and parsers Create *Parser and *Formatter interfaces and classes. Add a ParserFactory, probably need a Formatter factory. Fixes #6 --- README.md | 16 +- .../jsoup/BasicWhitelistConfiguration.java | 195 ++++++++++------ core/src/main/java/io/shick/jsoup/Func.java | 49 ---- .../jsoup/MutableWhitelistConfiguration.java | 8 +- .../shick/jsoup/WhitelistConfiguration.java | 34 ++- .../WhitelistConfigurationFormatter.java | 5 + .../jsoup/WhitelistConfigurationParser.java | 16 ++ .../WhitelistConfigurationParserFactory.java | 50 +++++ .../main/java/io/shick/jsoup/util/Func.java | 74 +++++++ .../BasicWhitelistConfigurationTest.java | 209 ++++++++++++++++++ ...itelistConfigurationParserFactoryTest.java | 28 +++ .../java/io/shick/jsoup/util/FuncTest.java | 90 ++++++++ .../jsoup/GsonWhitelistConfiguration.java | 27 --- .../io/shick/jsoup/gson/GsonFormatter.java | 66 ++++++ .../java/io/shick/jsoup/gson/GsonParser.java | 30 +++ .../gson/GsonWhitelistConfiguration.java | 14 ++ .../shick/jsoup/gson/GsonFormatterTest.java | 104 +++++++++ .../io/shick/jsoup/gson/GsonParserTest.java | 198 +++++++++++++++++ .../GsonWhitelistConfigurationTest.java | 65 ++++-- ...itelistConfigurationParserFactoryTest.java | 23 ++ .../jsoup/JowliMLWhitelistConfiguration.java | 31 --- .../shick/jsoup/jowli/JowliMLFormatter.java | 124 +++++++++++ .../jowli/{parser => }/JowliMLParser.java | 73 ++++-- .../jowli/JowliMLWhitelistConfiguration.java | 14 ++ .../io/shick/jsoup/jowli/ast/AllowedTags.java | 14 +- .../JowliMLWhitelistConfigurationTest.java | 60 ----- .../jsoup/jowli/JowliMLFormatterTest.java | 118 ++++++++++ .../jowli/{parser => }/JowliMLParserTest.java | 56 ++++- .../JowliMLWhitelistConfigurationTest.java | 24 ++ ...itelistConfigurationParserFactoryTest.java | 23 ++ .../jowli/ast/AllowedAttributesTest.java | 2 +- .../jsoup/jowli/ast/AllowedTagsTest.java | 2 +- 32 files changed, 1555 insertions(+), 287 deletions(-) delete mode 100644 core/src/main/java/io/shick/jsoup/Func.java create mode 100644 core/src/main/java/io/shick/jsoup/WhitelistConfigurationFormatter.java create mode 100644 core/src/main/java/io/shick/jsoup/WhitelistConfigurationParser.java create mode 100644 core/src/main/java/io/shick/jsoup/WhitelistConfigurationParserFactory.java create mode 100644 core/src/main/java/io/shick/jsoup/util/Func.java create mode 100644 core/src/test/java/io/shick/jsoup/BasicWhitelistConfigurationTest.java create mode 100644 core/src/test/java/io/shick/jsoup/WhitelistConfigurationParserFactoryTest.java create mode 100644 core/src/test/java/io/shick/jsoup/util/FuncTest.java delete mode 100644 gson/src/main/java/io/shick/jsoup/GsonWhitelistConfiguration.java create mode 100644 gson/src/main/java/io/shick/jsoup/gson/GsonFormatter.java create mode 100644 gson/src/main/java/io/shick/jsoup/gson/GsonParser.java create mode 100644 gson/src/main/java/io/shick/jsoup/gson/GsonWhitelistConfiguration.java create mode 100644 gson/src/test/java/io/shick/jsoup/gson/GsonFormatterTest.java create mode 100644 gson/src/test/java/io/shick/jsoup/gson/GsonParserTest.java rename gson/src/test/java/io/shick/jsoup/{ => gson}/GsonWhitelistConfigurationTest.java (74%) create mode 100644 gson/src/test/java/io/shick/jsoup/gson/WhitelistConfigurationParserFactoryTest.java delete mode 100644 jowli/src/main/java/io/shick/jsoup/JowliMLWhitelistConfiguration.java create mode 100644 jowli/src/main/java/io/shick/jsoup/jowli/JowliMLFormatter.java rename jowli/src/main/java/io/shick/jsoup/jowli/{parser => }/JowliMLParser.java (52%) create mode 100644 jowli/src/main/java/io/shick/jsoup/jowli/JowliMLWhitelistConfiguration.java delete mode 100644 jowli/src/test/java/io/shick/jsoup/JowliMLWhitelistConfigurationTest.java create mode 100644 jowli/src/test/java/io/shick/jsoup/jowli/JowliMLFormatterTest.java rename jowli/src/test/java/io/shick/jsoup/jowli/{parser => }/JowliMLParserTest.java (59%) create mode 100644 jowli/src/test/java/io/shick/jsoup/jowli/JowliMLWhitelistConfigurationTest.java create mode 100644 jowli/src/test/java/io/shick/jsoup/jowli/WhitelistConfigurationParserFactoryTest.java diff --git a/README.md b/README.md index 9131eea..c2abd8a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,21 @@ Then in your Java code ```java -final Whitelist whitelist = GsonWhitelistConfiguration.fromJson(json).whitelist(); +// you can simply instantiate the parser +final Whitelist whitelist = new GsonParser().parse(json).whitelist(); + +// or you can get a parser by 'type', (either gson or jowli) +final Whitelist whitelist = WhitelistConfigurationParserFactory.newParser("gson").parse(json).whitelist(); + +// or you can append to an existing whitelist +final Whitelist whitelist = new GsonParser().parse(json).apply(Whitelist.basic()); + +// you can construct a new config and serialize it out too! +WhitelistConfiguration wlc = new BasicWhitelistConfiguration().enforceAttribute("a","rel","nofollow"); + +final String jowli = new JowliMLFormatter().format(wlc).toString(); //jowliml +final String json = new GsonFormatter().format(wlc).toString(); //json + ``` diff --git a/core/src/main/java/io/shick/jsoup/BasicWhitelistConfiguration.java b/core/src/main/java/io/shick/jsoup/BasicWhitelistConfiguration.java index 6e447a6..a7344f6 100644 --- a/core/src/main/java/io/shick/jsoup/BasicWhitelistConfiguration.java +++ b/core/src/main/java/io/shick/jsoup/BasicWhitelistConfiguration.java @@ -1,124 +1,154 @@ package io.shick.jsoup; -import static io.shick.jsoup.Func.hashMap; -import static io.shick.jsoup.Func.list; +import static io.shick.jsoup.util.Func.hashMap; +import static io.shick.jsoup.util.Func.list; import static java.util.Objects.requireNonNull; +import io.shick.jsoup.util.Func; + import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import org.jsoup.safety.Whitelist; -abstract class BasicWhitelistConfiguration implements MutableWhitelistConfiguration { +public class BasicWhitelistConfiguration implements MutableWhitelistConfiguration { + + private List tags = null; + + private Map> attributes = null; + + private Map> enforcedAttributes = null; + + private Map>> protocols = null; + + private List tags() { + return Optional.ofNullable(tags).orElseGet(Collections::emptyList); + } + + private Map> attributes() { + return Optional.ofNullable(attributes).orElseGet(Collections::emptyMap); + } + + private Map> enforcedAttributes() { + return Optional.ofNullable(enforcedAttributes).orElseGet(Collections::emptyMap); + } + + private Map>> protocols() { + return Optional.ofNullable(protocols).orElseGet(Collections::emptyMap); + } - private List tags = new ArrayList<>(); + public MutableWhitelistConfiguration allowTag(String tagName) { + if (tags == null) { + tags = new ArrayList(); + } + tags().add(tagName); + return this; + } - private Map> attributes = new HashMap<>(); + public void allowedTags(Consumer fn) { + tags().forEach(fn); + } - private Map> enforcedAttributes = new HashMap<>(); + public void allowedAttributes(BiConsumer> fn) { + this.attributes().forEach(fn); + } - private Map>> protocols = new HashMap<>(); + public void enforcedAttributes(BiConsumer> fn) { + enforcedAttributes().forEach(fn); + } - public void allowTag(String tagName) { - tags.add(tagName); + public void allowedProtocols(BiConsumer>> fn) { + protocols().forEach(fn); } public boolean allowsTag(String tagName) { requireTagName(tagName); - return tags != null - && tags.contains(tagName); + return tags().contains(tagName); } public boolean hasAllowedAttributes(String tagName) { requireTagName(tagName); - return attributes != null - && attributes.containsKey(tagName); + return attributes().containsKey(tagName); } - public void allowAttribute(String tagName, String attrName) { + public MutableWhitelistConfiguration allowAttribute(String tagName, String attrName) { requireTagName(tagName); requireAttrName(attrName); - attributes.merge(tagName, list(attrName), Func::conj); + if (attributes == null) { + attributes = new HashMap(); + } + attributes().merge(tagName, list(attrName), Func::concat); + return this; } - - public boolean allowsAttribute(String tagName, String attrName) { requireAttrName(attrName); return hasAllowedAttributes(tagName) - && attributes.get(tagName) != null - && attributes.get(tagName).contains(attrName); + && attributes().get(tagName).contains(attrName); } public boolean hasEnforcedAttributes(String tagName) { requireTagName(tagName); - return enforcedAttributes != null - && enforcedAttributes.containsKey(tagName); + return enforcedAttributes().containsKey(tagName); } public boolean enforcesAttribute(String tagName, String attrName) { requireAttrName(attrName); return hasEnforcedAttributes(tagName) - && enforcedAttributes.get(tagName) != null - && enforcedAttributes.get(tagName).get(attrName) != null; + && enforcedAttributes().get(tagName).get(attrName) != null; } public boolean enforcesAttribute(String tagName, String attrName, String enforcedValue) { requireEnforcedValue(enforcedValue); return enforcesAttribute(tagName, attrName) - && enforcedAttributes.get(tagName).get(attrName).equals(enforcedValue); + && enforcedAttributes().get(tagName).get(attrName).equals(enforcedValue); } - public void enforceAttribute(String tagName, String attrName, String enforcedValue) { + public MutableWhitelistConfiguration enforceAttribute(String tagName, String attrName, String enforcedValue) { requireTagName(tagName); requireAttrName(attrName); requireEnforcedValue(enforcedValue); - enforcedAttributes.merge(tagName, hashMap(attrName, enforcedValue), Func::merge1); + if (enforcedAttributes == null) { + enforcedAttributes = new HashMap(); + } + enforcedAttributes().merge(tagName, hashMap(attrName, enforcedValue), Func::merge1); + return this; } public boolean hasAllowedProtocols(String tagName) { requireTagName(tagName); - return protocols != null - && protocols.containsKey(tagName); + return protocols().containsKey(tagName); } public boolean hasAllowedProtocols(String tagName, String attrName) { requireAttrName(attrName); return hasAllowedProtocols(tagName) - && protocols.get(tagName) != null - && protocols.get(tagName).containsKey(attrName); + && protocols().get(tagName).containsKey(attrName); } - public void allowProtocol(String tagName, String attrName, String protocol) { + public MutableWhitelistConfiguration allowProtocol(String tagName, String attrName, String protocol) { requireTagName(tagName); requireAttrName(attrName); requireProtocol(protocol); + if (protocols == null) { + protocols = new HashMap(); + } - protocols.merge(tagName, hashMap(attrName, list(protocol)), Func::merge2); + protocols().merge(tagName, hashMap(attrName, list(protocol)), Func::merge2); + return this; } public boolean allowsProtocol(String tagName, String attrName, String protocol) { requireProtocol(protocol); return hasAllowedProtocols(tagName, attrName) - && protocols.get(tagName).get(attrName).contains(protocol); - } - - private void requireTagName(String tagName) { - requireNonNull(tagName, "tagName cannot be null"); - } - - private void requireAttrName(String attrName) { - requireNonNull(attrName, "attrName cannot be null"); - } - - private void requireEnforcedValue(String enforcedValueCannot) { - requireNonNull(enforcedValueCannot, "enforcedValueCannot be null"); - } - - private void requireProtocol(String protocol) { - requireNonNull(protocol, "protocol cannot be null"); + && protocols().get(tagName).get(attrName).contains(protocol); } @Override @@ -135,28 +165,50 @@ public Whitelist whitelist() { return apply(Whitelist.none()); } - private void applyTags(Whitelist in) { - if (tags == null) { - return; + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BasicWhitelistConfiguration)) { + return false; } - in.addTags(tags.toArray(new String[tags.size()])); + BasicWhitelistConfiguration that = (BasicWhitelistConfiguration) o; + return Objects.equals(tags(), that.tags()) && + Objects.equals(attributes(), that.attributes()) && + Objects.equals(enforcedAttributes(), that.enforcedAttributes()) && + Objects.equals(protocols(), that.protocols()); + } + + @Override + public int hashCode() { + return Objects.hash(tags(), attributes(), enforcedAttributes(), protocols()); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("BasicWhitelistConfiguration{"); + sb.append("tags=").append(tags); + sb.append(", attributes=").append(attributes); + sb.append(", enforcedAttributes=").append(enforcedAttributes); + sb.append(", protocols=").append(protocols); + sb.append('}'); + return sb.toString(); + } + + private void applyTags(Whitelist in) { + in.addTags(tags().toArray(new String[tags().size()])); } private void applyAttributes(Whitelist in) { - if (attributes == null) { - return; - } - for (Map.Entry> attributeEntry : attributes.entrySet()) { + for (Map.Entry> attributeEntry : attributes().entrySet()) { in.addAttributes(attributeEntry.getKey(), attributeEntry.getValue().toArray(new String[attributeEntry.getValue().size()])); } } private void applyEnforcedAttributes(Whitelist in) { - if (enforcedAttributes == null) { - return; - } - for (Map.Entry> enforcedTag : enforcedAttributes.entrySet()) { + for (Map.Entry> enforcedTag : enforcedAttributes().entrySet()) { final String tag = enforcedTag.getKey(); for (Map.Entry enforcedAttribute : enforcedTag.getValue().entrySet()) { final String attribute = enforcedAttribute.getKey(); @@ -170,10 +222,7 @@ private void applyEnforcedAttributes(Whitelist in) { } private void applyProtocols(Whitelist in) { - if (protocols == null) { - return; - } - for (Map.Entry>> enforcedTag : protocols.entrySet()) { + for (Map.Entry>> enforcedTag : protocols().entrySet()) { final String tag = enforcedTag.getKey(); for (Map.Entry> enforcedAttribute : enforcedTag.getValue().entrySet()) { final String attribute = enforcedAttribute.getKey(); @@ -185,4 +234,20 @@ private void applyProtocols(Whitelist in) { } } } + + private void requireTagName(String tagName) { + requireNonNull(tagName, "tagName cannot be null"); + } + + private void requireAttrName(String attrName) { + requireNonNull(attrName, "attrName cannot be null"); + } + + private void requireEnforcedValue(String enforcedValueCannot) { + requireNonNull(enforcedValueCannot, "enforcedValueCannot be null"); + } + + private void requireProtocol(String protocol) { + requireNonNull(protocol, "protocol cannot be null"); + } } diff --git a/core/src/main/java/io/shick/jsoup/Func.java b/core/src/main/java/io/shick/jsoup/Func.java deleted file mode 100644 index 6d3f9bd..0000000 --- a/core/src/main/java/io/shick/jsoup/Func.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.shick.jsoup; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -/** - * Created by tshick on 11/10/16. - */ -public class Func { - - public static final List conj(List l, List r) { - final ArrayList a = new ArrayList<>(l.size() + r.size()); - a.addAll(l); - a.addAll(r); - return a; - } - - public static final Map merge1(Map l, Map r) { - Map m = new HashMap(); - m.putAll(l); - m.putAll(r); - return m; - } - - public static final Map> merge2(Map> l, Map> r) { - Map> m = new HashMap(); - m.putAll(l); - r.keySet().forEach(k -> { - m.merge(k, r.get(k), Func::conj); - }); - return m; - } - - public static final Map hashMap(K k, V v) { - Map m = new HashMap(); - m.put(k, v); - return m; - } - - public static final List list(T ... values) { - final List linkedList = new LinkedList(); - linkedList.addAll(Arrays.asList(values)); - return linkedList; - } -} diff --git a/core/src/main/java/io/shick/jsoup/MutableWhitelistConfiguration.java b/core/src/main/java/io/shick/jsoup/MutableWhitelistConfiguration.java index 98c380f..1d3165a 100644 --- a/core/src/main/java/io/shick/jsoup/MutableWhitelistConfiguration.java +++ b/core/src/main/java/io/shick/jsoup/MutableWhitelistConfiguration.java @@ -1,11 +1,11 @@ package io.shick.jsoup; public interface MutableWhitelistConfiguration extends WhitelistConfiguration { - void allowTag(String tagName); - void enforceAttribute(String tagName, String attrName, String enforcedValue); + MutableWhitelistConfiguration allowTag(String tagName); - void allowProtocol(String tagName, String attrName, String protocol); - void allowAttribute(String tagName, String attrName); + MutableWhitelistConfiguration enforceAttribute(String tagName, String attrName, String enforcedValue); + MutableWhitelistConfiguration allowProtocol(String tagName, String attrName, String protocol); + MutableWhitelistConfiguration allowAttribute(String tagName, String attrName); } diff --git a/core/src/main/java/io/shick/jsoup/WhitelistConfiguration.java b/core/src/main/java/io/shick/jsoup/WhitelistConfiguration.java index a1376a3..95efffa 100644 --- a/core/src/main/java/io/shick/jsoup/WhitelistConfiguration.java +++ b/core/src/main/java/io/shick/jsoup/WhitelistConfiguration.java @@ -1,11 +1,41 @@ package io.shick.jsoup; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + import org.jsoup.safety.Whitelist; public interface WhitelistConfiguration { + + void allowedTags(Consumer fn); + + void allowedAttributes(BiConsumer> fn); + + void enforcedAttributes(BiConsumer> fn); + + void allowedProtocols(BiConsumer>> fn); + + boolean allowsTag(String tagName); + + boolean hasAllowedAttributes(String tagName); + + boolean allowsAttribute(String tagName, String attrName); + + boolean hasEnforcedAttributes(String tagName); + + boolean enforcesAttribute(String tagName, String attrName); + + boolean enforcesAttribute(String tagName, String attrName, String enforcedValue); + + boolean hasAllowedProtocols(String tagName); + + boolean hasAllowedProtocols(String tagName, String attrName); + + boolean allowsProtocol(String tagName, String attrName, String protocol); + Whitelist apply(Whitelist in); Whitelist whitelist(); - - String format(WhitelistConfiguration config); } diff --git a/core/src/main/java/io/shick/jsoup/WhitelistConfigurationFormatter.java b/core/src/main/java/io/shick/jsoup/WhitelistConfigurationFormatter.java new file mode 100644 index 0000000..c862e0a --- /dev/null +++ b/core/src/main/java/io/shick/jsoup/WhitelistConfigurationFormatter.java @@ -0,0 +1,5 @@ +package io.shick.jsoup; + +public interface WhitelistConfigurationFormatter { + CharSequence format(WhitelistConfiguration config); +} diff --git a/core/src/main/java/io/shick/jsoup/WhitelistConfigurationParser.java b/core/src/main/java/io/shick/jsoup/WhitelistConfigurationParser.java new file mode 100644 index 0000000..3904d52 --- /dev/null +++ b/core/src/main/java/io/shick/jsoup/WhitelistConfigurationParser.java @@ -0,0 +1,16 @@ +package io.shick.jsoup; + +import java.text.ParseException; + +public interface WhitelistConfigurationParser { + /** + * @param value (non null) + * @return a {@link WhitelistConfiguration} instance + * @throws NullPointerException if value is null + * @throws java.text.ParseException if value is un-parseable + */ + WhitelistConfiguration parse(CharSequence value) throws + NullPointerException, + ParseException; + +} diff --git a/core/src/main/java/io/shick/jsoup/WhitelistConfigurationParserFactory.java b/core/src/main/java/io/shick/jsoup/WhitelistConfigurationParserFactory.java new file mode 100644 index 0000000..4f79016 --- /dev/null +++ b/core/src/main/java/io/shick/jsoup/WhitelistConfigurationParserFactory.java @@ -0,0 +1,50 @@ +package io.shick.jsoup; + +import static java.util.Objects.requireNonNull; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.logging.Logger; +import java.util.stream.Stream; + +public class WhitelistConfigurationParserFactory { + private static final Logger LOG = Logger.getLogger("jsoup-configuration"); + private static final Map> PARSERS = new HashMap<>(); + + public static final Collection registeredParserTypes() { + return PARSERS.keySet(); + } + + public static void register(String type, Supplier parserSupplier) { + LOG.fine("Registering parser factory of type " + type); + + PARSERS.put(requireNonNull(type, "type cannot be null"), + requireNonNull(parserSupplier, "parserSupplier cannot be null")); + } + + public static final WhitelistConfigurationParser newParser(String type) { + final Supplier factory = PARSERS.get(requireNonNull(type, "type cannot be null")); + if (factory == null) { + throw new IllegalArgumentException(type + " is not a registered parser type."); + } + return factory.get(); + } + + static { + // TODO - replace with some sort of scanning + Consumer load = (clazz) -> { + try { + Class.forName(clazz); + } + catch (ClassNotFoundException e) { + } + }; + + Stream.of( + "io.shick.jsoup.gson.GsonParser", + "io.shick.jsoup.jowli.JowliMLParser").forEach(load); + } +} diff --git a/core/src/main/java/io/shick/jsoup/util/Func.java b/core/src/main/java/io/shick/jsoup/util/Func.java new file mode 100644 index 0000000..0b47eba --- /dev/null +++ b/core/src/main/java/io/shick/jsoup/util/Func.java @@ -0,0 +1,74 @@ +package io.shick.jsoup.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Func { + + /** + * Append the two lists. + * @param l1 the starting list + * @param l2 the list to append + * @return + */ + public static final List concat(final List l1, final List l2) { + if (empty(l1) && empty(l2)) { return new ArrayList(0); } + if (!empty(l1) && empty(l2)) { return new ArrayList(l1); } + if (!empty(l2) && empty(l1)) { return new ArrayList(l2); } + + final ArrayList a = new ArrayList<>(l1.size() + l2.size()); + a.addAll(l1); + a.addAll(l2); + return a; + } + + public static boolean empty(Collection l1) { + return l1 == null || l1.isEmpty(); + } + + public static final Map merge1(Map l, Map r) { + if (empty(l) && empty(r)) { return new HashMap(); } + if (!empty(l) && empty(r)) { return new HashMap(l); } + if (!empty(r) && empty(l)) { return new HashMap(r); } + + Map m = new HashMap(); + m.putAll(l); + m.putAll(r); + return m; + } + + public static boolean empty(Map m) { + return m == null || m.isEmpty(); + } + + public static final Map> merge2(Map> l, Map> r) { + if (empty(l) && empty(r)) { return new HashMap(); } + if (!empty(l) && empty(r)) { return new HashMap(l); } + if (!empty(r) && empty(l)) { return new HashMap(r); } + + Map> m = new HashMap(); + m.putAll(l); + r.keySet().forEach(k -> { + m.merge(k, r.get(k), Func::concat); + }); + return m; + } + + public static final Map hashMap(K k, V v) { + Map m = new HashMap(); + m.put(k, v); + return m; + } + + public static final List list(T... values) { + if (values == null) return new ArrayList(0); + return new ArrayList<>(Stream.of(values).filter(Objects::nonNull).collect(Collectors.toList())); + } + +} diff --git a/core/src/test/java/io/shick/jsoup/BasicWhitelistConfigurationTest.java b/core/src/test/java/io/shick/jsoup/BasicWhitelistConfigurationTest.java new file mode 100644 index 0000000..0acd7d4 --- /dev/null +++ b/core/src/test/java/io/shick/jsoup/BasicWhitelistConfigurationTest.java @@ -0,0 +1,209 @@ +package io.shick.jsoup; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import io.shick.jsoup.util.Func; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Whitelist; +import org.junit.Test; + +public class BasicWhitelistConfigurationTest { + + final BasicWhitelistConfiguration config = new BasicWhitelistConfiguration(); + + @Test + public void tags() { + assertThat(config.allowsTag("a"), is(false)); + config.allowTag("a"); + assertThat(config.allowsTag("a"), is(true)); + + config.allowTag("b"); + config.allowTag("c"); + List x = new ArrayList(); + config.allowedTags(x::add); + assertThat(x, is(Func.list("a", "b", "c"))); + } + + @Test + public void allowedAttributes() { + assertThat(config.allowsAttribute("a", "href"), is(false)); + config.allowAttribute("a", "href"); + assertThat(config.allowsAttribute("a", "href"), is(true)); + assertThat(config.allowsAttribute("a", "not-href"), is(false)); + + config.allowAttribute("blockquote", "cite"); + config.allowAttribute("a", "rel"); + + Map> x = new HashMap(); + config.allowedAttributes(x::put); + + assertThat(x.size(), is(2)); + assertThat(x.containsKey("a"), is(true)); + assertThat(x.get("a"), is(Func.list("href", "rel"))); + + assertThat(x.containsKey("blockquote"), is(true)); + assertThat(x.get("blockquote"), is(Func.list("cite"))); + } + + @Test + public void enforceAttribute() { + assertThat(config.enforcesAttribute("a", "rel", "nofollow"), is(false)); + config.enforceAttribute("a", "rel", "nofollow"); + assertThat(config.enforcesAttribute("a", "rel", "nofollow"), is(true)); + assertThat(config.enforcesAttribute("a", "rel", "not-nofollow"), is(false)); + + assertThat(config.enforcesAttribute("a", "rel"), is(true)); + assertThat(config.enforcesAttribute("a", "not-rel"), is(false)); + + config.enforceAttribute("a", "href", "z"); + config.enforceAttribute("blockquote", "cite", "noone"); + + Map> x = new HashMap(); + config.enforcedAttributes(x::put); + + assertThat(x.size(), is(2)); + assertThat(x.containsKey("a"), is(true)); + assertThat(x.containsKey("blockquote"), is(true)); + + assertThat(x.get("a").get("href"), is("z")); + assertThat(x.get("a").get("rel"), is("nofollow")); + + assertThat(x.get("blockquote").get("cite"), is("noone")); + + + } + + @Test + public void protocol() { + assertThat(config.allowsProtocol("a", "href", "mailto"), is(false)); + config.allowProtocol("a", "href", "mailto"); + assertThat(config.allowsProtocol("a", "href", "mailto"), is(true)); + assertThat(config.allowsProtocol("a", "href", "not-mailto"), is(false)); + + assertThat(config.hasAllowedProtocols("a", "href"), is(true)); + assertThat(config.hasAllowedProtocols("a", "not-href"), is(false)); + + + config.allowProtocol("a", "href", "http"); + config.allowProtocol("img", "src", "https"); + + Map>> x = new HashMap(); + config.allowedProtocols(x::put); + + assertThat(x.size(), is(2)); + assertThat(x.containsKey("a"), is(true)); + assertThat(x.containsKey("img"), is(true)); + + assertThat(x.get("a").get("href"), is(Func.list("mailto","http"))); + assertThat(x.get("img").get("src"), is(Func.list("https"))); + } + + @Test + public void apply() { + final Whitelist wl = mock(Whitelist.class); + + final MutableWhitelistConfiguration c = new BasicWhitelistConfiguration() + .allowTag("a") + .allowTag("b") + .allowAttribute("blockquote", "cite") + .enforceAttribute("a", "rel", "nofollow") + .allowProtocol("a", "href", "ftp") + .allowProtocol("a", "href", "http") + .allowProtocol("a", "href", "https") + .allowProtocol("a", "href", "mailto"); + + c.apply(wl); + + verify(wl).addTags(new String[]{"a", "b"}); + verify(wl).addAttributes("blockquote", new String[]{"cite"}); + verify(wl).addEnforcedAttribute("a", "rel", "nofollow"); + verify(wl).addProtocols("a", "href", new String[]{"ftp", "http", "https", "mailto"}); + + } + + @Test + public void bigThree() { + final MutableWhitelistConfiguration o1 = new BasicWhitelistConfiguration() + .allowTag("a") + .allowAttribute("blockquote", "cite") + .enforceAttribute("a", "rel", "nofollow") + .allowProtocol("a", "href", "mailto"); + + final MutableWhitelistConfiguration o2 = new BasicWhitelistConfiguration() + .allowTag("a") + .allowAttribute("blockquote", "cite") + .enforceAttribute("a", "rel", "nofollow") + .allowProtocol("a", "href", "mailto"); + + final MutableWhitelistConfiguration o3 = new BasicWhitelistConfiguration() + .allowTag("b") + .allowAttribute("blockquote", "cite") + .enforceAttribute("a", "rel", "nofollow") + .allowProtocol("a", "href", "mailto"); + + assertThat(o1.equals(o1),is(true)); + assertThat(o1.equals(null),is(false)); + assertThat(o1.equals("non config"),is(false)); + + assertThat(o1, is(equalTo(o2))); + assertThat(o1.hashCode(), is(o2.hashCode())); + assertThat(o1.toString(), is(o2.toString())); + + assertThat(o1, is(not(o3))); + assertThat(o1.hashCode(), is(not(o3.hashCode()))); + assertThat(o1.toString(), is(not(o3.toString()))); + } + + @Test + public void whitelist() { + + final Whitelist whitelist = new BasicWhitelistConfiguration() + .allowTag("a") + .allowTag("b") + .allowAttribute("blockquote", "cite") + .enforceAttribute("a", "rel", "nofollow") + .allowProtocol("a", "href", "ftp") + .allowProtocol("a", "href", "http") + .allowProtocol("a", "href", "https") + .allowProtocol("a", "href", "mailto") + .whitelist(); + + assertThat("Allowed Tag", Jsoup.isValid("test", whitelist), is(true)); + assertThat("Allowed Tag", Jsoup.isValid("test", whitelist), is(true)); + + assertThat("Disallowed tag", Jsoup.isValid("test", whitelist), is(false)); + + assertThat("Allowed Attribute", Jsoup.isValid("
test
", whitelist), is(true)); + + assertThat("Disallowed Attribute", Jsoup.isValid("
test
", whitelist), is(false)); + + assertThat("Allowed Protocol", Jsoup.isValid("test", whitelist), is(true)); + assertThat("Disallowed Protocol", Jsoup.isValid("test", whitelist), is(false)); + + final Document.OutputSettings settings = new Document.OutputSettings().prettyPrint(false); + + assertThat("Clean Disallowed Attribute", + Jsoup.clean("
test
", "", whitelist, settings), + is("
test
")); + + assertThat("Clean Enforced Attribute", + Jsoup.clean("test", "", whitelist, settings), + is("test")); + + assertThat("Clean Disallowed Protocol", + Jsoup.clean("test", "", whitelist, settings), + is("test")); + } +} diff --git a/core/src/test/java/io/shick/jsoup/WhitelistConfigurationParserFactoryTest.java b/core/src/test/java/io/shick/jsoup/WhitelistConfigurationParserFactoryTest.java new file mode 100644 index 0000000..f6ac2f3 --- /dev/null +++ b/core/src/test/java/io/shick/jsoup/WhitelistConfigurationParserFactoryTest.java @@ -0,0 +1,28 @@ +package io.shick.jsoup; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +import org.junit.Test; + +public class WhitelistConfigurationParserFactoryTest { + + @Test(expected = IllegalArgumentException.class) + public void askForUnknownParserGetIAE() { + WhitelistConfigurationParserFactory.newParser("unknown"); + } + + @Test + public void registrationAndInstantiation() { + assertThat("the core has no access to any parser,so none are registered", + WhitelistConfigurationParserFactory.registeredParserTypes().isEmpty(), + is(true)); + + + final WhitelistConfigurationParser parser = mock(WhitelistConfigurationParser.class); + WhitelistConfigurationParserFactory.register("wowzer", () -> parser); + + assertThat(WhitelistConfigurationParserFactory.newParser("wowzer"), is(parser)); + } +} diff --git a/core/src/test/java/io/shick/jsoup/util/FuncTest.java b/core/src/test/java/io/shick/jsoup/util/FuncTest.java new file mode 100644 index 0000000..58a6a6b --- /dev/null +++ b/core/src/test/java/io/shick/jsoup/util/FuncTest.java @@ -0,0 +1,90 @@ +package io.shick.jsoup.util; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +public class FuncTest { + @Test + public void concat() throws Exception { + assertThat(Func.concat(null,null), is(equalTo(new ArrayList()))); + assertThat(Func.concat(null,Func.list("a")), is(equalTo(Func.list("a")))); + assertThat(Func.concat(Func.list("a"),null), is(equalTo(Func.list("a")))); + assertThat(Func.concat(Func.list(1,2,3),Func.list(4,5,6)), is(equalTo(Func.list(1,2,3,4,5,6)))); + } + + @Test + public void merge1() throws Exception { + Map v1= Func.hashMap("3","4"); + Map v2 = Func.hashMap("1","2"); + + Map actual = Func.merge1(v1, v2); + assertThat(actual.size(),is(2)); + assertThat(actual.containsKey("3"), is(true)); + assertThat(actual.containsKey("1"), is(true)); + + actual = Func.merge1(v1, null); + assertThat(actual.size(),is(1)); + assertThat(actual.containsKey("3"), is(true)); + + actual = Func.merge1(null, v2); + assertThat(actual.size(),is(1)); + assertThat(actual.containsKey("1"), is(true)); + + actual = Func.merge1(null, null); + assertThat(actual.size(),is(0)); + } + + @Test + public void merge2() throws Exception { + Map> v1= Func.hashMap("3",Func.list("a","b")); + Map> v2 = Func.hashMap("1",Func.list("c","d")); + Map> v3 = Func.hashMap("3",Func.list("c","d")); + + Map> actual = Func.merge2(v1, v2); + assertThat(actual.size(),is(2)); + assertThat(actual.containsKey("3"), is(true)); + assertThat(actual.containsKey("1"), is(true)); + + actual = Func.merge2(v1, null); + assertThat(actual.size(),is(1)); + assertThat(actual.containsKey("3"), is(true)); + + actual = Func.merge2(null, v2); + assertThat(actual.size(),is(1)); + assertThat(actual.containsKey("1"), is(true)); + + actual = Func.merge2(null, null); + assertThat(actual.size(),is(0)); + + actual = Func.merge2(v1, v3); + assertThat(actual.size(),is(1)); + assertThat(actual.containsKey("3"), is(true)); + assertThat(actual.get("3"), is(Func.list("a","b","c","d"))); + } + + @Test + public void hashMap() throws Exception { + + } + + @Test + public void list() throws Exception { + final ArrayList expected = new ArrayList(); + expected.add(1); + expected.add(2); + expected.add(3); + + assertThat(Func.list(1,2,3), is(expected)); + + assertThat(Func.list(), is(new ArrayList())); + assertThat(Func.list(null), is(new ArrayList())); + assertThat(Func.list(null,null,null), is(new ArrayList())); + } +} diff --git a/gson/src/main/java/io/shick/jsoup/GsonWhitelistConfiguration.java b/gson/src/main/java/io/shick/jsoup/GsonWhitelistConfiguration.java deleted file mode 100644 index 89ebcab..0000000 --- a/gson/src/main/java/io/shick/jsoup/GsonWhitelistConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.shick.jsoup; - -import org.jsoup.safety.Whitelist; - -import com.google.gson.Gson; - -public class GsonWhitelistConfiguration extends BasicWhitelistConfiguration { - - public static Whitelist whitelistFromJson(String json) { - return fromJson(json).whitelist(); - } - - public static GsonWhitelistConfiguration fromJson(String json) { - return new Gson().fromJson(json, GsonWhitelistConfiguration.class); - } - - @Override - public String format(WhitelistConfiguration config) { - if (config instanceof BasicWhitelistConfiguration) { - return new Gson().toJson(config); - } - else { - throw new IllegalArgumentException("I can only format instances of BasicWhitelistConfiguration"); - } - } -} - diff --git a/gson/src/main/java/io/shick/jsoup/gson/GsonFormatter.java b/gson/src/main/java/io/shick/jsoup/gson/GsonFormatter.java new file mode 100644 index 0000000..277a1d2 --- /dev/null +++ b/gson/src/main/java/io/shick/jsoup/gson/GsonFormatter.java @@ -0,0 +1,66 @@ +package io.shick.jsoup.gson; + +import static java.util.Objects.requireNonNull; + +import io.shick.jsoup.WhitelistConfiguration; +import io.shick.jsoup.WhitelistConfigurationFormatter; +import io.shick.jsoup.util.Func; + +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Map; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +public class GsonFormatter implements WhitelistConfigurationFormatter { + @Override + public CharSequence format(WhitelistConfiguration configuration) { + return gson().toJson(requireNonNull(configuration, "configuration cannot be null")); + } + + Gson gson() { + return new GsonBuilder() + .registerTypeHierarchyAdapter(Collection.class, new CollectionAdapter()) + .registerTypeHierarchyAdapter(Map.class, new MapAdapter()) + .create(); + } + + static class CollectionAdapter implements JsonSerializer> { + @Override + public JsonElement serialize(Collection src, Type typeOfSrc, JsonSerializationContext context) { + if (Func.empty(src)) { + return null; + } + + final JsonArray array = new JsonArray(); + src.forEach(value -> { + array.add(context.serialize(value)); + }); + return array; + } + } + + static class MapAdapter implements JsonSerializer> { + @Override + public JsonElement serialize(Map src, Type typeOfSrc, JsonSerializationContext context) { + if (Func.empty(src)) { + return null; + } + + final JsonObject map = new JsonObject(); + + src.forEach((k, v) -> { + JsonElement value = context.serialize(v); + map.add(k, value); + }); + + return map; + } + } +} diff --git a/gson/src/main/java/io/shick/jsoup/gson/GsonParser.java b/gson/src/main/java/io/shick/jsoup/gson/GsonParser.java new file mode 100644 index 0000000..c75b37e --- /dev/null +++ b/gson/src/main/java/io/shick/jsoup/gson/GsonParser.java @@ -0,0 +1,30 @@ +package io.shick.jsoup.gson; + +import io.shick.jsoup.WhitelistConfiguration; +import io.shick.jsoup.WhitelistConfigurationParser; +import io.shick.jsoup.WhitelistConfigurationParserFactory; + +import java.text.ParseException; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + +public class GsonParser implements WhitelistConfigurationParser { + @Override + public WhitelistConfiguration parse(CharSequence value) throws ParseException { + try { + return gson().fromJson(value.toString(), GsonWhitelistConfiguration.class); + } catch (JsonSyntaxException jse) { + throw new ParseException(jse.getMessage(),0); + } + } + + private Gson gson() { + return new GsonBuilder().create(); + } + + static { + WhitelistConfigurationParserFactory.register("gson", GsonParser::new); + } +} diff --git a/gson/src/main/java/io/shick/jsoup/gson/GsonWhitelistConfiguration.java b/gson/src/main/java/io/shick/jsoup/gson/GsonWhitelistConfiguration.java new file mode 100644 index 0000000..85ff809 --- /dev/null +++ b/gson/src/main/java/io/shick/jsoup/gson/GsonWhitelistConfiguration.java @@ -0,0 +1,14 @@ +package io.shick.jsoup.gson; + +import io.shick.jsoup.BasicWhitelistConfiguration; + +public class GsonWhitelistConfiguration extends BasicWhitelistConfiguration { + + protected GsonWhitelistConfiguration() {} + + public String toString() { + return new GsonFormatter().format(this).toString(); + } + +} + diff --git a/gson/src/test/java/io/shick/jsoup/gson/GsonFormatterTest.java b/gson/src/test/java/io/shick/jsoup/gson/GsonFormatterTest.java new file mode 100644 index 0000000..11f527f --- /dev/null +++ b/gson/src/test/java/io/shick/jsoup/gson/GsonFormatterTest.java @@ -0,0 +1,104 @@ +package io.shick.jsoup.gson; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import io.shick.jsoup.BasicWhitelistConfiguration; +import io.shick.jsoup.WhitelistConfiguration; + +import java.text.ParseException; + +import org.junit.Test; + +public class GsonFormatterTest { + + @Test + public void fullCircleWithTags() throws ParseException { + WhitelistConfiguration wlc = new BasicWhitelistConfiguration() + .allowTag("blockquote") + .allowTag("b"); + final String json = new GsonFormatter().format(wlc).toString(); + assertThat(json, is("{\"tags\":[\"blockquote\",\"b\"]}")); + final WhitelistConfiguration wlc2 = new GsonParser().parse(json); + assertThat(wlc, is(wlc2)); + } + @Test + public void fullCircleWithAllowedAttributes() throws ParseException { + WhitelistConfiguration wlc = new BasicWhitelistConfiguration() + .allowAttribute("blockquote","cite") + .allowAttribute("a","href"); + final String json = new GsonFormatter().format(wlc).toString(); + assertThat(json, is("{\"attributes\":{\"a\":[\"href\"],\"blockquote\":[\"cite\"]}}")); + final WhitelistConfiguration wlc2 = new GsonParser().parse(json); + assertThat(wlc, is(wlc2)); + } + @Test + public void fullCircleWithEnforcedAttributes() throws ParseException { + WhitelistConfiguration wlc = new BasicWhitelistConfiguration() + .enforceAttribute("a","rel","nofollow"); + final String json = new GsonFormatter().format(wlc).toString(); + assertThat(json, is("{\"enforcedAttributes\":{\"a\":{\"rel\":\"nofollow\"}}}")); + final WhitelistConfiguration wlc2 = new GsonParser().parse(json); + assertThat(wlc, is(wlc2)); + } + @Test + public void fullCircleWithProtocols() throws ParseException { + WhitelistConfiguration wlc = new BasicWhitelistConfiguration() + .allowProtocol("a","href","mailto"); + final String json = new GsonFormatter().format(wlc).toString(); + assertThat(json, is( "{\"protocols\":{\"a\":{\"href\":[\"mailto\"]}}}")); + final WhitelistConfiguration wlc2 = new GsonParser().parse(json); + assertThat(wlc, is(wlc2)); + } + @Test + public void format() throws ParseException { + final String json="\n" + + "{\n" + + " \"tags\" : [\"a\",\"b\"],\n" + + " \"attributes\" : {\n" + + " \"blockquote\": [\"cite\"]\n" + + " },\n" + + " \"enforcedAttributes\": {\n" + + " \"a\" : {\n" + + " \"rel\" : \"nofollow\"\n" + + " }\n" + + " },\n" + + " \"protocols\" : {\n" + + " \"a\" : { \n" + + " \"href\":[\"ftp\", \"http\", \"https\", \"mailto\"]\n" + + " }\n" + + " }\n" + + "}"; + + final WhitelistConfiguration config = new GsonParser().parse(json); + final String actual = new GsonFormatter().format(config).toString(); + final WhitelistConfiguration config2 = new GsonParser().parse(actual); + + assertThat(stripped(json),is(stripped(actual))); + assertThat(config, is(config2)); + } + + + @Test + public void emptyCollectionsAreNull() throws ParseException { + final String json="\n" + + "{\n" + + " \"tags\" : [],\n" + + " \"attributes\" : {},\n" + + " \"enforcedAttributes\": {},\n" + + " \"protocols\" : {}\n" + + "}"; + + final WhitelistConfiguration config = new GsonParser().parse(json); + final String actual = new GsonFormatter().format(config).toString(); + + final WhitelistConfiguration config2 = new GsonParser().parse(actual); + + assertThat(actual,is("{}")); + assertThat(config, is(config2)); + } + + private String stripped(String json) { + return json.replaceAll("[\\n\\s]",""); + } +} diff --git a/gson/src/test/java/io/shick/jsoup/gson/GsonParserTest.java b/gson/src/test/java/io/shick/jsoup/gson/GsonParserTest.java new file mode 100644 index 0000000..7aa880d --- /dev/null +++ b/gson/src/test/java/io/shick/jsoup/gson/GsonParserTest.java @@ -0,0 +1,198 @@ +package io.shick.jsoup.gson; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import io.shick.jsoup.WhitelistConfiguration; + +import java.text.ParseException; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Whitelist; +import org.junit.Test; + +public class GsonParserTest { + + @Test(expected = ParseException.class) + public void badJsonYieldsParseException() throws ParseException { + new GsonParser().parse("bad json"); + } + + @Test + public void verifyEmptyCollectionsNotNulls() throws ParseException { + String json = "{\"tags\" : null, \"attributes\":null }"; + + final WhitelistConfiguration c = new GsonParser().parse(json); + assertThat(c.allowsTag("a"), is(false)); + assertThat(c.allowsAttribute("x","any"), is(false)); + + verifyNoNpes(c); + } + + + + @Test + public void parseTags() throws ParseException { + String json = "{\"tags\" : [\"a\",\"b\"]}"; + + final WhitelistConfiguration c = new GsonParser().parse(json); + assertThat(c.allowsTag("a"), is(true)); + assertThat(c.allowsTag("b"), is(true)); + assertThat(c.allowsTag("c"), is(false)); + verifyNoNpes(c); + } + + @Test + public void parseAttributes() throws ParseException { + String json = "{" + + " \"attributes\" : {\n" + + " \"blockquote\": [\"cite\"]\n" + + " }}"; + + final WhitelistConfiguration c = new GsonParser().parse(json); + assertThat(c.hasAllowedAttributes("blockquote"), is(true)); + assertThat(c.hasAllowedAttributes("something-else"), is(false)); + + assertThat(c.allowsAttribute("blockquote", "cite"), is(true)); + assertThat(c.allowsAttribute("blockquote", "cited"), is(false)); + assertThat(c.allowsAttribute("something-else", "cite"), is(false)); + verifyNoNpes(c); + } + + @Test + public void enforcedAttributes() throws ParseException { + String json = "{ \"enforcedAttributes\": {\n" + + " \"a\" : {\n" + + " \"rel\" : \"nofollow\"\n" + + " }\n" + + " }\n}"; + + final WhitelistConfiguration c = new GsonParser().parse(json); + assertThat(c.hasEnforcedAttributes("a"), is(true)); + assertThat(c.hasEnforcedAttributes("something-else"), is(false)); + + assertThat(c.enforcesAttribute("a", "rel"), is(true)); + assertThat(c.enforcesAttribute("a", "rel", "nofollow"), is(true)); + assertThat(c.enforcesAttribute("a", "rel", "something-else"), is(false)); + assertThat(c.enforcesAttribute("blockquote", "cited"), is(false)); + assertThat(c.enforcesAttribute("something-else", "cite"), is(false)); + assertThat(c.enforcesAttribute("something-else", "cite", "something-else"), is(false)); + verifyNoNpes(c); + } + + // "protocols" : { + @Test + public void protocols() throws ParseException { + String json = "{\n" + + " \"protocols\" : {\n" + + " \"a\" : { \n" + + " \"href\":[\"ftp\", \"http\", \"https\", \"mailto\"]\n" + + " }\n" + + " }\n" + + "}"; + + final WhitelistConfiguration c = new GsonParser().parse(json); + assertThat(c.hasAllowedProtocols("a"), is(true)); + assertThat(c.hasAllowedProtocols("a", "href"), is(true)); + assertThat(c.allowsProtocol("a", "href", "ftp"), is(true)); + + assertThat(c.hasAllowedProtocols("not-a"), is(false)); + assertThat(c.hasAllowedProtocols("a", "not-href"), is(false)); + assertThat(c.hasAllowedProtocols("not-a", "href"), is(false)); + assertThat(c.allowsProtocol("not-a", "href", "ftp"), is(false)); + assertThat(c.allowsProtocol("a", "not-href", "ftp"), is(false)); + assertThat(c.allowsProtocol("a", "href", "not-ftp"), is(false)); + + verifyNoNpes(c); + } + + private void verifyNoNpes(WhitelistConfiguration c) { + assertThat("ensure no NPEs", c.allowsTag("tag"), is(false)); + assertThat("ensure no NPEs", c.hasEnforcedAttributes("tag"), is(false)); + assertThat("ensure no NPEs", c.hasAllowedAttributes("tag"), is(false)); + assertThat("ensure no NPEs", c.hasAllowedProtocols("tag"), is(false)); + } + + @Test + public void apply() throws ParseException { + final String json = "\n" + + "{\n" + + " \"tags\" : [\"a\",\"b\"],\n" + + " \"attributes\" : {\n" + + " \"blockquote\": [\"cite\"]\n" + + " },\n" + + " \"enforcedAttributes\": {\n" + + " \"a\" : {\n" + + " \"rel\" : \"nofollow\"\n" + + " }\n" + + " },\n" + + " \"protocols\" : {\n" + + " \"a\" : { \n" + + " \"href\":[\"ftp\", \"http\", \"https\", \"mailto\"]\n" + + " }\n" + + " }\n" + + "}"; + + final WhitelistConfiguration c = new GsonParser().parse(json); + final Whitelist wl = mock(Whitelist.class); + + c.apply(wl); + + verify(wl).addTags(new String[]{"a", "b"}); + verify(wl).addAttributes("blockquote", new String[]{"cite"}); + verify(wl).addEnforcedAttribute("a", "rel", "nofollow"); + verify(wl).addProtocols("a", "href", new String[]{"ftp", "http", "https", "mailto"}); + } + + @Test + public void whitelist() throws ParseException { + final String json = "\n" + + "{\n" + + " \"tags\" : [\"a\",\"b\"],\n" + + " \"attributes\" : {\n" + + " \"blockquote\": [\"cite\"]\n" + + " },\n" + + " \"enforcedAttributes\": {\n" + + " \"a\" : {\n" + + " \"rel\" : \"nofollow\"\n" + + " }\n" + + " },\n" + + " \"protocols\" : {\n" + + " \"a\" : { \n" + + " \"href\":[\"ftp\", \"http\", \"https\", \"mailto\"]\n" + + " }\n" + + " }\n" + + "}"; + + final Whitelist whitelist = new GsonParser().parse(json).whitelist(); + + assertThat("Allowed Tag", Jsoup.isValid("test", whitelist), is(true)); + assertThat("Allowed Tag", Jsoup.isValid("test", whitelist), is(true)); + + assertThat("Disallowed tag", Jsoup.isValid("test", whitelist), is(false)); + + assertThat("Allowed Attribute", Jsoup.isValid("
test
", whitelist), is(true)); + + assertThat("Disallowed Attribute", Jsoup.isValid("
test
", whitelist), is(false)); + + assertThat("Allowed Protocol", Jsoup.isValid("test", whitelist), is(true)); + assertThat("Disallowed Protocol", Jsoup.isValid("test", whitelist), is(false)); + + final Document.OutputSettings settings = new Document.OutputSettings().prettyPrint(false); + + assertThat("Clean Disallowed Attribute", + Jsoup.clean("
test
", "", whitelist, settings), + is("
test
")); + + assertThat("Clean Enforced Attribute", + Jsoup.clean("test", "", whitelist, settings), + is("test")); + + assertThat("Clean Disallowed Protocol", + Jsoup.clean("test", "", whitelist, settings), + is("test")); + } +} diff --git a/gson/src/test/java/io/shick/jsoup/GsonWhitelistConfigurationTest.java b/gson/src/test/java/io/shick/jsoup/gson/GsonWhitelistConfigurationTest.java similarity index 74% rename from gson/src/test/java/io/shick/jsoup/GsonWhitelistConfigurationTest.java rename to gson/src/test/java/io/shick/jsoup/gson/GsonWhitelistConfigurationTest.java index f03c29a..9c6db0a 100644 --- a/gson/src/test/java/io/shick/jsoup/GsonWhitelistConfigurationTest.java +++ b/gson/src/test/java/io/shick/jsoup/gson/GsonWhitelistConfigurationTest.java @@ -1,10 +1,14 @@ -package io.shick.jsoup; +package io.shick.jsoup.gson; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import io.shick.jsoup.WhitelistConfiguration; + +import java.text.ParseException; + import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.safety.Whitelist; @@ -13,10 +17,10 @@ public class GsonWhitelistConfigurationTest { @Test - public void parseTags() { + public void parseTags() throws ParseException { String json = "{\"tags\" : [\"a\",\"b\"]}"; - final GsonWhitelistConfiguration c = GsonWhitelistConfiguration.fromJson(json); + final WhitelistConfiguration c = new GsonParser().parse(json); assertThat(c.allowsTag("a"), is(true)); assertThat(c.allowsTag("b"), is(true)); assertThat(c.allowsTag("c"), is(false)); @@ -24,13 +28,13 @@ public void parseTags() { } @Test - public void parseAttributes() { + public void parseAttributes() throws ParseException { String json = "{" + " \"attributes\" : {\n" + " \"blockquote\": [\"cite\"]\n" + " }}"; - final GsonWhitelistConfiguration c = GsonWhitelistConfiguration.fromJson(json); + final WhitelistConfiguration c = new GsonParser().parse(json); assertThat(c.hasAllowedAttributes("blockquote"), is(true)); assertThat(c.hasAllowedAttributes("something-else"), is(false)); @@ -42,14 +46,14 @@ public void parseAttributes() { } @Test - public void enforcedAttributes() { + public void enforcedAttributes() throws ParseException { String json = "{ \"enforcedAttributes\": {\n" + " \"a\" : {\n" + " \"rel\" : \"nofollow\"\n" + " }\n" + " }\n}"; - final GsonWhitelistConfiguration c = GsonWhitelistConfiguration.fromJson(json); + final WhitelistConfiguration c = new GsonParser().parse(json); assertThat(c.hasEnforcedAttributes("a"), is(true)); assertThat(c.hasEnforcedAttributes("something-else"), is(false)); @@ -64,7 +68,7 @@ public void enforcedAttributes() { } // "protocols" : { @Test - public void protocols() { + public void protocols() throws ParseException { String json = "{\n" + " \"protocols\" : {\n" + " \"a\" : { \n" @@ -73,7 +77,7 @@ public void protocols() { + " }\n" + "}"; - final GsonWhitelistConfiguration c = GsonWhitelistConfiguration.fromJson(json); + final WhitelistConfiguration c = new GsonParser().parse(json); assertThat(c.hasAllowedProtocols("a"), is(true)); assertThat(c.hasAllowedProtocols("a", "href"), is(true)); assertThat(c.allowsProtocol("a","href","ftp"), is(true)); @@ -88,7 +92,7 @@ public void protocols() { verifyNoNpes(c); } - private void verifyNoNpes(GsonWhitelistConfiguration c) { + private void verifyNoNpes(WhitelistConfiguration c) { assertThat("ensure no NPEs", c.allowsTag("tag"), is(false)); assertThat("ensure no NPEs", c.hasEnforcedAttributes("tag"), is(false)); assertThat("ensure no NPEs", c.hasAllowedAttributes("tag"), is(false)); @@ -96,7 +100,7 @@ private void verifyNoNpes(GsonWhitelistConfiguration c) { } @Test - public void apply() { + public void apply() throws ParseException { final String json="\n" + "{\n" + " \"tags\" : [\"a\",\"b\"],\n" @@ -115,7 +119,7 @@ public void apply() { + " }\n" + "}"; - final GsonWhitelistConfiguration c = GsonWhitelistConfiguration.fromJson(json); + final WhitelistConfiguration c = new GsonParser().parse(json); final Whitelist wl = mock(Whitelist.class); c.apply(wl); @@ -128,7 +132,7 @@ public void apply() { } @Test - public void whitelist() { + public void whitelist() throws ParseException { final String json="\n" + "{\n" + " \"tags\" : [\"a\",\"b\"],\n" @@ -147,7 +151,8 @@ public void whitelist() { + " }\n" + "}"; - final Whitelist whitelist = GsonWhitelistConfiguration.whitelistFromJson(json); + final Whitelist whitelist = new GsonParser().parse(json).whitelist(); + assertThat("Allowed Tag", Jsoup.isValid("test", whitelist), is(true)); assertThat("Allowed Tag", Jsoup.isValid("test", whitelist), is(true)); @@ -176,5 +181,37 @@ public void whitelist() { is("test")); } + + @Test + public void format() throws ParseException { + final String json="\n" + + "{\n" + + " \"tags\" : [\"a\",\"b\"],\n" + + " \"attributes\" : {\n" + + " \"blockquote\": [\"cite\"]\n" + + " },\n" + + " \"enforcedAttributes\": {\n" + + " \"a\" : {\n" + + " \"rel\" : \"nofollow\"\n" + + " }\n" + + " },\n" + + " \"protocols\" : {\n" + + " \"a\" : { \n" + + " \"href\":[\"ftp\", \"http\", \"https\", \"mailto\"]\n" + + " }\n" + + " }\n" + + "}"; + + final WhitelistConfiguration config = new GsonParser().parse(json); + final String actual = new GsonFormatter().format(config).toString(); + final WhitelistConfiguration config2 = new GsonParser().parse(actual); + assertThat(actual, is(config.toString())); + assertThat(stripped(json),is(stripped(actual))); + assertThat(config, is(config2)); + } + + private String stripped(String json) { + return json.replaceAll("[\\n\\s]",""); + } } diff --git a/gson/src/test/java/io/shick/jsoup/gson/WhitelistConfigurationParserFactoryTest.java b/gson/src/test/java/io/shick/jsoup/gson/WhitelistConfigurationParserFactoryTest.java new file mode 100644 index 0000000..40a79a3 --- /dev/null +++ b/gson/src/test/java/io/shick/jsoup/gson/WhitelistConfigurationParserFactoryTest.java @@ -0,0 +1,23 @@ +package io.shick.jsoup.gson; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +import io.shick.jsoup.WhitelistConfigurationParserFactory; + +import org.junit.Test; + +public class WhitelistConfigurationParserFactoryTest { + + @Test + public void test() { + assertThat("Only the gson parser is loaded", + WhitelistConfigurationParserFactory.registeredParserTypes().size(), + is(1)); + + assertThat(WhitelistConfigurationParserFactory.newParser("gson"), + is(not(nullValue()))); + } +} diff --git a/jowli/src/main/java/io/shick/jsoup/JowliMLWhitelistConfiguration.java b/jowli/src/main/java/io/shick/jsoup/JowliMLWhitelistConfiguration.java deleted file mode 100644 index dfee576..0000000 --- a/jowli/src/main/java/io/shick/jsoup/JowliMLWhitelistConfiguration.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.shick.jsoup; - -import static io.shick.jsoup.jowli.parser.JowliMLParser.ROOT; - -import org.jsoup.safety.Whitelist; - -public class JowliMLWhitelistConfiguration extends BasicWhitelistConfiguration { - - public static Whitelist whitelistFromJowliML(String jowliml) { - return fromJowliML(jowliml).whitelist(); - } - - public static JowliMLWhitelistConfiguration fromJowliML(String json) { - final JowliMLWhitelistConfiguration c = new JowliMLWhitelistConfiguration(); - - ROOT.parse(json).stream() - .forEach(consumer -> consumer.accept(c)); - return c; - } - - - @Override - public String format(WhitelistConfiguration config) { - if (config instanceof BasicWhitelistConfiguration) { - return "not implemented"; - } - else { - throw new IllegalArgumentException("I can only format instances of BasicWhitelistConfiguration"); - } - } -} diff --git a/jowli/src/main/java/io/shick/jsoup/jowli/JowliMLFormatter.java b/jowli/src/main/java/io/shick/jsoup/jowli/JowliMLFormatter.java new file mode 100644 index 0000000..9829f47 --- /dev/null +++ b/jowli/src/main/java/io/shick/jsoup/jowli/JowliMLFormatter.java @@ -0,0 +1,124 @@ +package io.shick.jsoup.jowli; + +import static java.util.Objects.requireNonNull; + +import io.shick.jsoup.WhitelistConfiguration; +import io.shick.jsoup.WhitelistConfigurationFormatter; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +public class JowliMLFormatter implements WhitelistConfigurationFormatter { + + public CharSequence format(WhitelistConfiguration configuration) { + final AtomicReference ref = new AtomicReference<>(); + root(requireNonNull(configuration, "configuration cannot be null"), ref::set); + return Optional.ofNullable(ref.get()).orElse(""); + } + + private void root(WhitelistConfiguration config, Consumer c) { + final StringJoiner sj = new StringJoiner(";"); + tagsDirective(config, sj::add); + allowedAttributesDirective(config, sj::add); + enforcedAttributesDirective(config, sj::add); + protocolsDirective(config, sj::add); + if (sj.length() > 0) { + c.accept(sj.toString()); + } + } + + /** + * "a:blockquote[cite],a[href,rel]" + * + * @param config + * @param c + */ + private void allowedAttributesDirective(WhitelistConfiguration config, Consumer c) { + final StringJoiner sj = new StringJoiner(","); + + config.allowedAttributes((tag, attrs) -> { + sj.add(new StringBuilder(tag) + .append(bracketed(join(attrs, ",")))); + }); + + if (sj.length() > 0) { + c.accept(new StringBuilder("a:").append(sj.toString())); + } + } + + /** + * "e:a[rel:nofollow,x:y]" + * + * @param config + * @param c + */ + private void enforcedAttributesDirective(WhitelistConfiguration config, Consumer c) { + final StringJoiner sj = new StringJoiner(","); + + config.enforcedAttributes((tag, attrs) -> { + final List attrValues = new LinkedList<>(); + attrs.forEach((k, v) -> attrValues.add(colonSeparated(k, v))); + + sj.add(new StringBuilder(tag) + .append(bracketed(join(attrValues, ",")))); + }); + + if (sj.length() > 0) { + c.accept(new StringBuilder("e:").append(sj.toString())); + } + } + + /** + * "p:a[href:[ftp,http,https],z:[d]]" + * + * @param config + * @param c + */ + private void protocolsDirective(WhitelistConfiguration config, Consumer c) { + final StringJoiner sj = new StringJoiner(","); + + config.allowedProtocols((tag, attrToProtList) -> { + final StringJoiner attrs = new StringJoiner(","); + attrToProtList.forEach((attr, protlist) -> { + attrs.add(colonSeparated(attr, bracketed(join(protlist, ",")))); + }); + sj.add(new StringBuilder(tag).append(bracketed(attrs.toString()))); + }); + + if (sj.length() > 0) { + c.accept(new StringBuilder("p:").append(sj.toString())); + } + } + + /** + * "t:a,b" + * + * @param config + * @param c + */ + private void tagsDirective(WhitelistConfiguration config, Consumer c) { + final StringJoiner sj = new StringJoiner(","); + config.allowedTags(sj::add); + if (sj.length() > 0) { + c.accept(new StringBuilder("t:").append(sj.toString())); + } + } + + private String join(List attrs, String delimiter) { + final StringJoiner sj = new StringJoiner(delimiter); + attrs.forEach(sj::add); + return sj.toString(); + } + + private String bracketed(String string) { + return new StringBuilder("[").append(string).append("]").toString(); + } + + private String colonSeparated(String s, String s1) { + return new StringBuilder(s).append(":").append(s1).toString(); + } +} diff --git a/jowli/src/main/java/io/shick/jsoup/jowli/parser/JowliMLParser.java b/jowli/src/main/java/io/shick/jsoup/jowli/JowliMLParser.java similarity index 52% rename from jowli/src/main/java/io/shick/jsoup/jowli/parser/JowliMLParser.java rename to jowli/src/main/java/io/shick/jsoup/jowli/JowliMLParser.java index 48bd78c..728dd39 100644 --- a/jowli/src/main/java/io/shick/jsoup/jowli/parser/JowliMLParser.java +++ b/jowli/src/main/java/io/shick/jsoup/jowli/JowliMLParser.java @@ -1,119 +1,144 @@ -package io.shick.jsoup.jowli.parser; +package io.shick.jsoup.jowli; import static org.codehaus.jparsec.Parsers.between; import static org.codehaus.jparsec.Parsers.sequence; import static org.codehaus.jparsec.Scanners.isChar; -import io.shick.jsoup.jowli.ast.ConfigConsumer; +import io.shick.jsoup.WhitelistConfiguration; +import io.shick.jsoup.WhitelistConfigurationParser; +import io.shick.jsoup.WhitelistConfigurationParserFactory; import io.shick.jsoup.jowli.ast.AllowedAttributes; import io.shick.jsoup.jowli.ast.AllowedTags; import io.shick.jsoup.jowli.ast.Attr; +import io.shick.jsoup.jowli.ast.ConfigConsumer; import io.shick.jsoup.jowli.ast.EnforcedAttributes; import io.shick.jsoup.jowli.ast.Prot; import io.shick.jsoup.jowli.ast.Protocols; import io.shick.jsoup.jowli.ast.Tag; +import java.text.ParseException; import java.util.List; import org.codehaus.jparsec.Parser; import org.codehaus.jparsec.Parsers; +import org.codehaus.jparsec.error.ParserException; import org.codehaus.jparsec.functors.Pair; import org.codehaus.jparsec.functors.Tuples; import org.codehaus.jparsec.pattern.Pattern; import org.codehaus.jparsec.pattern.Patterns; -public final class JowliMLParser { +public final class JowliMLParser implements WhitelistConfigurationParser { - public static final Parser RIGHT_BRACKET = isChar(']'); - public static final Parser LEFT_BRACKET = isChar('['); + static final Parser RIGHT_BRACKET = isChar(']'); + static final Parser LEFT_BRACKET = isChar('['); - public static final Parser COMMA = isChar(','); - public static final Parser COLON = isChar(':'); + static final Parser COMMA = isChar(','); + static final Parser COLON = isChar(':'); - public static final Pattern ALPHA_TOKEN = Patterns.regex("[a-zA-Z]+"); + static final Pattern ALPHA_TOKEN = Patterns.regex("[a-zA-Z]+"); - public static final Parser PROTOCOL_NAME = + static final Parser PROTOCOL_NAME = ALPHA_TOKEN.toScanner("protocol name").source().map(Prot::new); - public static final Parser TAG_NAME = + static final Parser TAG_NAME = ALPHA_TOKEN.toScanner("tag name").source().map(Tag::new); - public static final Parser ATTR_NAME = + static final Parser ATTR_NAME = ALPHA_TOKEN.toScanner("attribute name").source().map(Attr::new); - public static final Parser ENFORCED_VALUE = Patterns.regex("[^\\],]+") + static final Parser ENFORCED_VALUE = Patterns.regex("[^\\],]+") .toScanner("enforced value").source(); - private static Parser bracketed(Parser parser) { + static Parser bracketed(Parser parser) { return between(LEFT_BRACKET, parser, RIGHT_BRACKET); } - private static Parser> commaed(Parser p) { + static Parser> commaed(Parser p) { return p.sepBy(COMMA); } - public static final Parser>> ATTR_PROTOCOL_NAMES = + static final Parser>> ATTR_PROTOCOL_NAMES = sequence(ATTR_NAME, COLON, bracketed(commaed(PROTOCOL_NAME)), (name, __, value) -> Tuples.pair(name, value)); - public static final Parser> ATTR_ENFORCED_VALUE = + static final Parser> ATTR_ENFORCED_VALUE = sequence(ATTR_NAME, COLON, ENFORCED_VALUE, (name, __, value) -> Tuples.pair(name, value)); - public static final Parser>> TAG_LIST_OF_ATTR_NAMES = + static final Parser>> TAG_LIST_OF_ATTR_NAMES = sequence( TAG_NAME, bracketed(commaed(ATTR_NAME)), Tuples::pair); - public static final Parser>>>> TAG_ATTR_PROTOCOLS = + static final Parser>>>> TAG_ATTR_PROTOCOLS = sequence( TAG_NAME, bracketed(commaed(ATTR_PROTOCOL_NAMES)), (name, list) -> Tuples.pair(name, list)); - public static final Parser>>> TAG_ATTR_ENFORCED_VALUES = + static final Parser>>> TAG_ATTR_ENFORCED_VALUES = sequence( TAG_NAME, bracketed(commaed(ATTR_ENFORCED_VALUE)), (name, list) -> Tuples.pair(name, list)); - public static final Parser ALLOWED_TAGS_DIRECTIVE = + static final Parser ALLOWED_TAGS_DIRECTIVE = sequence( isChar('t'), COLON, commaed(TAG_NAME), (__, ___, v) -> new AllowedTags(v)); - public static final Parser ALLOWED_ATTRIBUTES_DIRECTIVE = + static final Parser ALLOWED_ATTRIBUTES_DIRECTIVE = sequence( isChar('a'), COLON, commaed(TAG_LIST_OF_ATTR_NAMES), (__, ___, v) -> new AllowedAttributes(v)); - public static final Parser ENFORCED_ATTRIBUTES_DIRECTIVE = + static final Parser ENFORCED_ATTRIBUTES_DIRECTIVE = sequence( isChar('e'), COLON, commaed(TAG_ATTR_ENFORCED_VALUES), (__, ___, v) -> new EnforcedAttributes(v)); - public static final Parser PROTOCOLS_DIRECTIVE = + static final Parser PROTOCOLS_DIRECTIVE = sequence( isChar('p'), COLON, commaed(TAG_ATTR_PROTOCOLS), (__, ___, v) -> new Protocols(v)); - public static final Parser> ROOT = + static final Parser> ROOT = Parsers.or( ALLOWED_ATTRIBUTES_DIRECTIVE, ENFORCED_ATTRIBUTES_DIRECTIVE, PROTOCOLS_DIRECTIVE, ALLOWED_TAGS_DIRECTIVE).sepBy(isChar(';')); + + + public WhitelistConfiguration parse(CharSequence value) throws ParseException { + return parse(new JowliMLWhitelistConfiguration(), value); + } + + public WhitelistConfiguration parse(JowliMLWhitelistConfiguration c, CharSequence from) throws ParseException { + try { + ROOT.parse(from).stream() + .forEach(consumer -> consumer.accept(c)); + return c; + } catch (ParserException p) { + throw new ParseException(p.getMessage(), p.getLocation().column); + } + } + + static { + WhitelistConfigurationParserFactory.register("jowli", JowliMLParser::new); + } + } diff --git a/jowli/src/main/java/io/shick/jsoup/jowli/JowliMLWhitelistConfiguration.java b/jowli/src/main/java/io/shick/jsoup/jowli/JowliMLWhitelistConfiguration.java new file mode 100644 index 0000000..61623e4 --- /dev/null +++ b/jowli/src/main/java/io/shick/jsoup/jowli/JowliMLWhitelistConfiguration.java @@ -0,0 +1,14 @@ +package io.shick.jsoup.jowli; + +import io.shick.jsoup.BasicWhitelistConfiguration; + +import java.text.ParseException; + +public class JowliMLWhitelistConfiguration extends BasicWhitelistConfiguration { + + public JowliMLWhitelistConfiguration() {} + + public String toString() { + return new JowliMLFormatter().format(this).toString(); + } +} diff --git a/jowli/src/main/java/io/shick/jsoup/jowli/ast/AllowedTags.java b/jowli/src/main/java/io/shick/jsoup/jowli/ast/AllowedTags.java index 1db9d7c..cdc62a5 100644 --- a/jowli/src/main/java/io/shick/jsoup/jowli/ast/AllowedTags.java +++ b/jowli/src/main/java/io/shick/jsoup/jowli/ast/AllowedTags.java @@ -6,12 +6,12 @@ public final class AllowedTags extends ValueObject> implements ConfigConsumer { - public AllowedTags(List v) { - super(v); - } + public AllowedTags(List v) { + super(v); + } - @Override - public void accept(MutableWhitelistConfiguration c) { - value().stream().map(Tag::value).forEach(c::allowTag); - } + @Override + public void accept(MutableWhitelistConfiguration c) { + value().stream().map(Tag::value).forEach(c::allowTag); } +} diff --git a/jowli/src/test/java/io/shick/jsoup/JowliMLWhitelistConfigurationTest.java b/jowli/src/test/java/io/shick/jsoup/JowliMLWhitelistConfigurationTest.java deleted file mode 100644 index a0f409a..0000000 --- a/jowli/src/test/java/io/shick/jsoup/JowliMLWhitelistConfigurationTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.shick.jsoup; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.safety.Whitelist; -import org.junit.Test; - -/** - * Created by tshick on 11/10/16. - */ -public class JowliMLWhitelistConfigurationTest { - - @Test - public void whitelist() { -// p:a[href:[ftp,http,https],a:b]; - - final StringBuilder jowliml = new StringBuilder() - .append("t:a,b") - .append(";") - .append("a:blockquote[cite],a[href,rel]") - .append(";") - .append("e:a[rel:nofollow,x:y]") - .append(";") - .append("p:a[href:[ftp,http,https],z:[d]]") - ; - final Whitelist whitelist = JowliMLWhitelistConfiguration.whitelistFromJowliML(jowliml.toString()); - - assertThat("Allowed Tag", Jsoup.isValid("test", whitelist), is(true)); - assertThat("Allowed Tag", Jsoup.isValid("test", whitelist), is(true)); - - assertThat("Disallowed tag", Jsoup.isValid("test", whitelist), is(false)); - - assertThat("Allowed Attribute", Jsoup.isValid("
test
", whitelist), is(true)); - - assertThat("Disallowed Attribute", Jsoup.isValid("
test
", whitelist), is(false)); - - assertThat("Allowed Protocol", Jsoup.isValid("test", whitelist), is(true)); - assertThat("Allowed Protocol", Jsoup.isValid("test", whitelist), is(true)); - - assertThat("Disallowed Protocol", Jsoup.isValid("test", whitelist), is(false)); - assertThat("Disallowed Protocol", Jsoup.isValid("test", whitelist), is(false)); - - final Document.OutputSettings settings = new Document.OutputSettings().prettyPrint(false); - - assertThat("Clean Disallowed Attribute", - Jsoup.clean("
test
", "", whitelist, settings), - is("
test
")); - - assertThat("Clean Enforced Attribute", - Jsoup.clean("test", "", whitelist, settings), - is("test")); - - assertThat("Clean Disallowed Protocol", - Jsoup.clean("test", "", whitelist, settings), - is("test")); - } -} diff --git a/jowli/src/test/java/io/shick/jsoup/jowli/JowliMLFormatterTest.java b/jowli/src/test/java/io/shick/jsoup/jowli/JowliMLFormatterTest.java new file mode 100644 index 0000000..4de580e --- /dev/null +++ b/jowli/src/test/java/io/shick/jsoup/jowli/JowliMLFormatterTest.java @@ -0,0 +1,118 @@ +package io.shick.jsoup.jowli; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import io.shick.jsoup.BasicWhitelistConfiguration; +import io.shick.jsoup.WhitelistConfiguration; + +import java.text.ParseException; + +import org.junit.Test; + +public class JowliMLFormatterTest { + + @Test(expected = NullPointerException.class) + public void nullThrowsException() { + new JowliMLFormatter().format(null); + } + + @Test + public void fullCircleWithNothing() throws ParseException { + WhitelistConfiguration wlc = new BasicWhitelistConfiguration(); + final String json = new JowliMLFormatter().format(wlc).toString(); + assertThat(json, is("")); + final WhitelistConfiguration wlc2 = new JowliMLParser().parse(json); + assertThat(wlc, is(wlc2)); + } + + @Test + public void fullCircleWithTags() throws ParseException { + WhitelistConfiguration wlc = new BasicWhitelistConfiguration() + .allowTag("blockquote") + .allowTag("b"); + final String json = new JowliMLFormatter().format(wlc).toString(); + assertThat(json, is("t:blockquote,b")); + final WhitelistConfiguration wlc2 = new JowliMLParser().parse(json); + assertThat(wlc, is(wlc2)); + } + @Test + public void fullCircleWithAllowedAttributes() throws ParseException { + WhitelistConfiguration wlc = new BasicWhitelistConfiguration() + .allowAttribute("blockquote","cite") + .allowAttribute("a","href"); + final String json = new JowliMLFormatter().format(wlc).toString(); + assertThat(json, is("a:a[href],blockquote[cite]")); + final WhitelistConfiguration wlc2 = new JowliMLParser().parse(json); + assertThat(wlc, is(wlc2)); + } + @Test + public void fullCircleWithEnforcedAttributes() throws ParseException { + WhitelistConfiguration wlc = new BasicWhitelistConfiguration() + .enforceAttribute("a","rel","nofollow"); + final String json = new JowliMLFormatter().format(wlc).toString(); + assertThat(json, is("e:a[rel:nofollow]")); + final WhitelistConfiguration wlc2 = new JowliMLParser().parse(json); + assertThat(wlc, is(wlc2)); + } + @Test + public void fullCircleWithProtocols() throws ParseException { + final WhitelistConfiguration wlc = new BasicWhitelistConfiguration() + .allowProtocol("a","href","mailto"); + + final String json = new JowliMLFormatter().format(wlc).toString(); + final WhitelistConfiguration wlc2 = new JowliMLParser().parse(json); + + assertThat(json, is("p:a[href:[mailto]]")); + assertThat(wlc, is(wlc2)); + } + + + @Test + public void formatJustTags() throws ParseException { + final String jowliml = "t:a,b"; + + verifyInAndOutAreSame(jowliml); + } + + @Test + public void formatAll() throws ParseException { + final String jowliml + = "t:a,b;a:a[href,rel],blockquote[cite];e:a[rel:nofollow,x:y];p:a[z:[d],href:[ftp,http,https]]"; + + verifyInAndOutAreSame(jowliml); + } + + @Test + public void formatJustProtocols() throws ParseException { + final String jowliml = "p:a[z:[d],href:[ftp,http,https]]"; + + verifyInAndOutAreSame(jowliml); + } + + @Test + public void formatJustAllowedAttributes() throws ParseException { + final String jowliml = "a:a[href,rel],blockquote[cite]"; + + verifyInAndOutAreSame(jowliml); + } + + @Test + public void formatJustEnforcedAttributes() throws ParseException { + final String jowliml = "e:a[rel:nofollow,x:y]"; + + verifyInAndOutAreSame(jowliml); + } + + private void verifyInAndOutAreSame(String jowliml) throws ParseException { + final WhitelistConfiguration config = new JowliMLParser().parse(jowliml); + final String actual = new JowliMLFormatter().format(config).toString(); + + assertThat(actual, is(equalTo(jowliml))); + + final WhitelistConfiguration config2 = new JowliMLParser().parse(actual); + assertThat(config, is(config2)); + } + +} diff --git a/jowli/src/test/java/io/shick/jsoup/jowli/parser/JowliMLParserTest.java b/jowli/src/test/java/io/shick/jsoup/jowli/JowliMLParserTest.java similarity index 59% rename from jowli/src/test/java/io/shick/jsoup/jowli/parser/JowliMLParserTest.java rename to jowli/src/test/java/io/shick/jsoup/jowli/JowliMLParserTest.java index f3da094..8f02f73 100644 --- a/jowli/src/test/java/io/shick/jsoup/jowli/parser/JowliMLParserTest.java +++ b/jowli/src/test/java/io/shick/jsoup/jowli/JowliMLParserTest.java @@ -1,4 +1,4 @@ -package io.shick.jsoup.jowli.parser; +package io.shick.jsoup.jowli; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; @@ -7,18 +7,72 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import io.shick.jsoup.MutableWhitelistConfiguration; +import io.shick.jsoup.WhitelistConfigurationParserFactory; import io.shick.jsoup.jowli.ast.AllowedAttributes; import io.shick.jsoup.jowli.ast.AllowedTags; import io.shick.jsoup.jowli.ast.ConfigConsumer; import io.shick.jsoup.jowli.ast.EnforcedAttributes; import io.shick.jsoup.jowli.ast.Protocols; +import java.text.ParseException; import java.util.List; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Whitelist; import org.junit.Test; public class JowliMLParserTest { + @Test(expected = ParseException.class) + public void invalidSyntaxYieldsParseException() throws ParseException { + WhitelistConfigurationParserFactory.newParser("jowli").parse("xxx"); + } + + @Test + public void whitelist() throws ParseException { + final StringBuilder jowliml = new StringBuilder() + .append("t:a,b") + .append(";") + .append("a:blockquote[cite],a[href,rel]") + .append(";") + .append("e:a[rel:nofollow,x:y]") + .append(";") + .append("p:a[href:[ftp,http,https],z:[d]]") + ; + final Whitelist whitelist = new JowliMLParser().parse(jowliml.toString()).whitelist(); + + assertThat("Allowed Tag", Jsoup.isValid("test", whitelist), is(true)); + assertThat("Allowed Tag", Jsoup.isValid("test", whitelist), is(true)); + + assertThat("Disallowed tag", Jsoup.isValid("test", whitelist), is(false)); + + assertThat("Allowed Attribute", Jsoup.isValid("
test
", whitelist), is(true)); + + assertThat("Disallowed Attribute", Jsoup.isValid("
test
", whitelist), is(false)); + + assertThat("Allowed Protocol", Jsoup.isValid("test", whitelist), is(true)); + assertThat("Allowed Protocol", Jsoup.isValid("test", whitelist), is(true)); + + assertThat("Disallowed Protocol", Jsoup.isValid("test", whitelist), is(false)); + assertThat("Disallowed Protocol", Jsoup.isValid("test", whitelist), is(false)); + + final Document.OutputSettings settings = new Document.OutputSettings().prettyPrint(false); + + assertThat("Clean Disallowed Attribute", + Jsoup.clean("
test
", "", whitelist, settings), + is("
test
")); + + assertThat("Clean Enforced Attribute", + Jsoup.clean("test", "", whitelist, settings), + is("test")); + + assertThat("Clean Disallowed Protocol", + Jsoup.clean("test", "", whitelist, settings), + is("test")); + } + + @Test public void allowedTags() { diff --git a/jowli/src/test/java/io/shick/jsoup/jowli/JowliMLWhitelistConfigurationTest.java b/jowli/src/test/java/io/shick/jsoup/jowli/JowliMLWhitelistConfigurationTest.java new file mode 100644 index 0000000..760592f --- /dev/null +++ b/jowli/src/test/java/io/shick/jsoup/jowli/JowliMLWhitelistConfigurationTest.java @@ -0,0 +1,24 @@ +package io.shick.jsoup.jowli; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import io.shick.jsoup.WhitelistConfiguration; + +import java.text.ParseException; + +import org.junit.Test; + +/** + * Created by tshick on 11/11/16. + */ +public class JowliMLWhitelistConfigurationTest { + + @Test + public void toStringFormatsJowli() throws ParseException { + String text = "t:a,b,c;a:dog[a,b,c]"; + final WhitelistConfiguration c = new JowliMLParser().parse(text); + + assertThat(c.toString(), is(new JowliMLFormatter().format(c))); + } +} diff --git a/jowli/src/test/java/io/shick/jsoup/jowli/WhitelistConfigurationParserFactoryTest.java b/jowli/src/test/java/io/shick/jsoup/jowli/WhitelistConfigurationParserFactoryTest.java new file mode 100644 index 0000000..7494034 --- /dev/null +++ b/jowli/src/test/java/io/shick/jsoup/jowli/WhitelistConfigurationParserFactoryTest.java @@ -0,0 +1,23 @@ +package io.shick.jsoup.jowli; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +import io.shick.jsoup.WhitelistConfigurationParserFactory; + +import org.junit.Test; + +public class WhitelistConfigurationParserFactoryTest { + + @Test + public void test() { + assertThat("Only the jowli parser", + WhitelistConfigurationParserFactory.registeredParserTypes().size(), + is(1)); + + assertThat(WhitelistConfigurationParserFactory.newParser("jowli"), + is(not(nullValue()))); + } +} diff --git a/jowli/src/test/java/io/shick/jsoup/jowli/ast/AllowedAttributesTest.java b/jowli/src/test/java/io/shick/jsoup/jowli/ast/AllowedAttributesTest.java index 284f1c8..c0a1d62 100644 --- a/jowli/src/test/java/io/shick/jsoup/jowli/ast/AllowedAttributesTest.java +++ b/jowli/src/test/java/io/shick/jsoup/jowli/ast/AllowedAttributesTest.java @@ -1,6 +1,6 @@ package io.shick.jsoup.jowli.ast; -import static io.shick.jsoup.Func.list; +import static io.shick.jsoup.util.Func.list; import static org.codehaus.jparsec.functors.Tuples.pair; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; diff --git a/jowli/src/test/java/io/shick/jsoup/jowli/ast/AllowedTagsTest.java b/jowli/src/test/java/io/shick/jsoup/jowli/ast/AllowedTagsTest.java index df51b5a..d3a4126 100644 --- a/jowli/src/test/java/io/shick/jsoup/jowli/ast/AllowedTagsTest.java +++ b/jowli/src/test/java/io/shick/jsoup/jowli/ast/AllowedTagsTest.java @@ -1,6 +1,6 @@ package io.shick.jsoup.jowli.ast; -import static io.shick.jsoup.Func.list; +import static io.shick.jsoup.util.Func.list; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions;