You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by pa...@apache.org on 2021/12/23 11:03:27 UTC

[sling-org-apache-sling-xss] 01/01: Remove antiysamy dep - still incomplete

This is an automated email from the ASF dual-hosted git repository.

pauls pushed a commit to branch noanitsamydep
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-xss.git

commit 7dfec26f001ba5d72167ea47dccbbd4bfaad07a0
Author: Karl Pauls <ka...@gmail.com>
AuthorDate: Thu Dec 23 12:02:51 2021 +0100

    Remove antiysamy dep - still incomplete
---
 pom.xml                                            |  10 +-
 .../org/apache/sling/xss/impl/FallbackATag.java    |  14 +-
 .../apache/sling/xss/impl/FallbackSlingPolicy.java |  34 +-
 .../java/org/apache/sling/xss/impl/XSSAPIImpl.java |   4 +
 .../org/apache/sling/xss/impl/XSSFilterImpl.java   |  23 +-
 .../java/org/owasp/validator/html/AntiSamy.java    |  17 +
 .../org/owasp/validator/html/CleanResults.java     |  17 +
 src/main/java/org/owasp/validator/html/Policy.java | 464 +++++++++++++++++++++
 .../org/owasp/validator/html/PolicyException.java  |   9 +
 .../org/owasp/validator/html/ScanException.java    |   4 +
 .../org/owasp/validator/html/model/Attribute.java  |  39 ++
 .../org/owasp/validator/html/model/Property.java   |  22 +
 .../java/org/owasp/validator/html/model/Tag.java   |  41 ++
 .../org/owasp/validator/html/util/XMLUtil.java     | 209 ++++++++++
 src/main/resources/antisamy.xsd                    | 147 +++++++
 15 files changed, 1001 insertions(+), 53 deletions(-)

diff --git a/pom.xml b/pom.xml
index 575edef..6c419e1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -185,12 +185,6 @@
     <!-- ======================================================================= -->
     <dependencies>
         <dependency>
-            <groupId>org.owasp.antisamy</groupId>
-            <artifactId>antisamy</artifactId>
-            <version>1.5.10</version>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
             <groupId>xml-apis</groupId>
             <artifactId>xml-apis</artifactId>
             <version>1.4.01</version>
@@ -216,6 +210,10 @@
                     <groupId>commons-collections</groupId>
                     <artifactId>commons-collections</artifactId>
                 </exclusion>
+                <exclusion>
+                    <groupId>org.owasp.antisamy</groupId>
+                    <artifactId>antisamy</artifactId>
+                </exclusion>
             </exclusions>
         </dependency>
 
diff --git a/src/main/java/org/apache/sling/xss/impl/FallbackATag.java b/src/main/java/org/apache/sling/xss/impl/FallbackATag.java
index a94de65..04b32b7 100644
--- a/src/main/java/org/apache/sling/xss/impl/FallbackATag.java
+++ b/src/main/java/org/apache/sling/xss/impl/FallbackATag.java
@@ -18,8 +18,6 @@
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
 package org.apache.sling.xss.impl;
 
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 
 import org.owasp.validator.html.model.Attribute;
@@ -27,16 +25,6 @@ import org.owasp.validator.html.model.Tag;
 
 public class FallbackATag extends Tag {
 
-    static final Attribute FALLBACK_HREF_ATTRIBUTE = new Attribute(
-            "href",
-            Arrays.asList(
-                    XSSFilterImpl.ON_SITE_SIMPLIFIED,
-                    XSSFilterImpl.OFF_SITE_SIMPLIFIED
-            ),
-            Collections.emptyList(),
-            "removeAttribute", ""
-    );
-
     private final Tag wrapped;
 
     public FallbackATag(Tag wrapped) {
@@ -72,7 +60,7 @@ public class FallbackATag extends Tag {
     @Override
     public Attribute getAttributeByName(String name) {
         if ("href".equalsIgnoreCase(name)) {
-            return FALLBACK_HREF_ATTRIBUTE;
+            return XSSFilterImpl.FALLBACK_HREF_ATTRIBUTE;
         }
         return wrapped.getAttributeByName(name);
     }
diff --git a/src/main/java/org/apache/sling/xss/impl/FallbackSlingPolicy.java b/src/main/java/org/apache/sling/xss/impl/FallbackSlingPolicy.java
index ddf4d0d..6213f2f 100644
--- a/src/main/java/org/apache/sling/xss/impl/FallbackSlingPolicy.java
+++ b/src/main/java/org/apache/sling/xss/impl/FallbackSlingPolicy.java
@@ -20,36 +20,18 @@ package org.apache.sling.xss.impl;
 
 import java.io.InputStream;
 
-import org.owasp.validator.html.InternalPolicy;
+import org.owasp.validator.html.Policy;
 import org.owasp.validator.html.PolicyException;
 import org.owasp.validator.html.model.Tag;
-import org.xml.sax.InputSource;
 
-public class FallbackSlingPolicy extends InternalPolicy {
-
-    private FallbackATag fallbackATag;
-    private final Object aTagLock = new Object();
+public class FallbackSlingPolicy extends Policy {
 
     public FallbackSlingPolicy(InputStream inputStream) throws PolicyException {
-       super(null, getSimpleParseContext(getTopLevelElement(new InputSource(inputStream))));
-
-    }
-
-    @Override
-    public Tag getTagByLowercaseName(String tagName) {
-        if ("a".equalsIgnoreCase(tagName)) {
-            synchronized (aTagLock) {
-                if (fallbackATag == null) {
-                    Tag wrapped = super.getTagByLowercaseName(tagName);
-                    if (wrapped != null) {
-                        fallbackATag = new FallbackATag(wrapped);
-                    }
-                }
-            }
-            if (fallbackATag != null) {
-                return fallbackATag;
-            }
-        }
-        return super.getTagByLowercaseName(tagName);
+       super(inputStream);
+       Tag original = getTagByLowercaseName("a");
+       if (original != null) {
+           Tag wrapped = new FallbackATag(original);
+           tagRules.put("a", wrapped);
+       }
     }
 }
diff --git a/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java b/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
index 68ed5c9..0dc45c7 100644
--- a/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
+++ b/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
@@ -351,7 +351,9 @@ public class XSSAPIImpl implements XSSAPI {
             return "";
         }
 
+        ClassLoader tccl = Thread.currentThread().getContextClassLoader();
         try {
+            Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
             SAXParser parser = factory.newSAXParser();
             XMLReader reader = parser.getXMLReader();
             reader.parse(new InputSource(new StringReader(xml)));
@@ -359,6 +361,8 @@ public class XSSAPIImpl implements XSSAPI {
         } catch (Exception e) {
             LOGGER.warn("Unable to get valid XML from the input.", e);
             LOGGER.debug("XML input:\n{}", xml);
+        } finally {
+            Thread.currentThread().setContextClassLoader(tccl);
         }
         return getValidXML(defaultXml, "");
     }
diff --git a/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java b/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java
index d11d18e..7a1b3e1 100644
--- a/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java
+++ b/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java
@@ -136,7 +136,15 @@ public class XSSFilterImpl implements XSSFilter {
     static final Pattern OFF_SITE_SIMPLIFIED = Pattern.compile("(\\s)*((ht|f)tp(s?)://|mailto:)" +
             "[\\p{L}\\p{N}]+[\\p{L}\\p{N}\\p{Zs}\\.\\#@\\$%\\+&amp;;:\\-_~,\\?=/!\\*\\(\\)]*(\\s)*");
 
-    private static final Pattern[] BACKUP_PATTERNS = new Pattern[] {ON_SITE_SIMPLIFIED, OFF_SITE_SIMPLIFIED};
+    static final Attribute FALLBACK_HREF_ATTRIBUTE = new Attribute(
+            "href",
+            Arrays.asList(
+                    ON_SITE_SIMPLIFIED,
+                    OFF_SITE_SIMPLIFIED
+            ),
+            Collections.emptyList(),
+            "removeAttribute", ""
+    );
 
     /*
       NumericEntityEscaper is deprecated starting with version 3.6 of commons-lang3, however the indicated replacement comes from
@@ -229,18 +237,17 @@ public class XSSFilterImpl implements XSSFilter {
 
     private boolean runHrefValidation(@NotNull String url) {
         // Same logic as in org.owasp.validator.html.scan.MagicSAXFilter.startElement()
-        boolean isValid = hrefAttribute.containsAllowedValue(url.toLowerCase());
+        String urlLowerCase = url.toLowerCase();
+        boolean isValid = hrefAttribute.containsAllowedValue(urlLowerCase);
         if (!isValid) {
             try {
-                isValid = hrefAttribute.matchesAllowedExpression(url.toLowerCase());
+                isValid = hrefAttribute.matchesAllowedExpression(urlLowerCase);
             } catch (StackOverflowError e) {
                 logger.debug("Detected a StackOverflowError when validating url {} with configured regexes. Trying fallback.", url);
                 try {
-                    for (Pattern p : BACKUP_PATTERNS) {
-                        isValid = p.matcher(url.toLowerCase()).matches();
-                        if (isValid) {
-                            break;
-                        }
+                    isValid = FALLBACK_HREF_ATTRIBUTE.containsAllowedValue(urlLowerCase);
+                    if (!isValid) {
+                        isValid = FALLBACK_HREF_ATTRIBUTE.matchesAllowedExpression(urlLowerCase);
                     }
                 } catch (StackOverflowError inner) {
                     logger.debug("Detected a StackOverflowError when validating url {} with fallback regexes", url);
diff --git a/src/main/java/org/owasp/validator/html/AntiSamy.java b/src/main/java/org/owasp/validator/html/AntiSamy.java
new file mode 100644
index 0000000..696de9a
--- /dev/null
+++ b/src/main/java/org/owasp/validator/html/AntiSamy.java
@@ -0,0 +1,17 @@
+package org.owasp.validator.html;
+
+public class AntiSamy {
+    public static final Object DOM = "DOM";
+    public static final Object SAX = "SAX";
+
+    public AntiSamy(Policy policy) {
+    }
+
+    public CleanResults scan(String input) throws ScanException {
+        throw new IllegalStateException();
+    }
+
+    public CleanResults scan(String input, Object dom) {
+        throw new IllegalStateException();
+    }
+}
diff --git a/src/main/java/org/owasp/validator/html/CleanResults.java b/src/main/java/org/owasp/validator/html/CleanResults.java
new file mode 100644
index 0000000..390f481
--- /dev/null
+++ b/src/main/java/org/owasp/validator/html/CleanResults.java
@@ -0,0 +1,17 @@
+package org.owasp.validator.html;
+
+import java.util.List;
+
+public class CleanResults {
+    public int getNumberOfErrors() {
+        throw new IllegalStateException();
+    }
+
+    public String getCleanHTML() {
+        throw new IllegalStateException();
+    }
+
+    public List<String> getErrorMessages() {
+        throw new IllegalStateException();
+    }
+}
diff --git a/src/main/java/org/owasp/validator/html/Policy.java b/src/main/java/org/owasp/validator/html/Policy.java
new file mode 100644
index 0000000..33d85a0
--- /dev/null
+++ b/src/main/java/org/owasp/validator/html/Policy.java
@@ -0,0 +1,464 @@
+package org.owasp.validator.html;
+
+import org.owasp.validator.html.model.Attribute;
+import org.owasp.validator.html.model.Property;
+import org.owasp.validator.html.model.Tag;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.Source;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import static org.owasp.validator.html.util.XMLUtil.getAttributeValue;
+
+public class Policy {
+    private static final String POLICY_SCHEMA_URI = "antisamy.xsd";
+
+    protected final Map<String, Pattern> commonRegularExpressions = new HashMap<>();
+    protected final Map<String, Attribute> commonAttributes = new HashMap<>();
+    protected final Map<String, Tag> tagRules = new HashMap<>();
+    protected final Map<String, Property> cssRules = new HashMap<>();
+    protected final Map<String, String> directives = new HashMap<>();
+    protected final Map<String, Attribute> globalAttributes = new HashMap<>();
+    protected final Map<String, Attribute> dynamicAttributes = new HashMap<>();
+    protected final List<String> allowedEmptyTags = new ArrayList<>();
+    protected final List<String> requireClosingTags = new ArrayList<>();
+
+    protected Policy(InputStream input) throws PolicyException {
+        Element root = getTopLevelElement(input);
+        init(root);
+    }
+
+    public static Policy getInstance(InputStream bais) throws PolicyException {
+        return new Policy(bais);
+    }
+
+    public Tag getTagByLowercaseName(String a) {
+        return tagRules.get(a);
+    }
+
+    private Element getTopLevelElement(InputStream input) throws PolicyException {
+        ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+        try {
+            Thread.currentThread().setContextClassLoader(Policy.class.getClassLoader());
+
+            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+            dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+            dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+            dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
+            dbf.setNamespaceAware(true);
+            InputStream schemaStream = Policy.class.getClassLoader().getResourceAsStream(POLICY_SCHEMA_URI);
+            Source schemaSource = new StreamSource(schemaStream);
+            Schema schema = null;
+            try {
+                schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
+                        .newSchema(schemaSource);
+            } catch (SAXException e) {
+                throw new PolicyException(e);
+            }
+            dbf.setSchema(schema);
+            DocumentBuilder db = dbf.newDocumentBuilder();
+            db.setErrorHandler(new SAXErrorHandler());
+            Document dom = db.parse(new InputSource(input));
+
+            Element element = dom.getDocumentElement();
+            return element;
+        } catch (Exception e) {
+            throw new PolicyException(e);
+        } finally {
+            Thread.currentThread().setContextClassLoader(tccl);
+        }
+    }
+
+    private void init(Element topLevelElement) throws PolicyException {
+        parseCommonRegExps(getFirstChild(topLevelElement, "common-regexps"), commonRegularExpressions);
+        parseDirectives(getFirstChild(topLevelElement, "directives"), directives);
+        parseCommonAttributes(getFirstChild(topLevelElement, "common-attributes"), commonAttributes, commonRegularExpressions);
+        parseGlobalAttributes(getFirstChild(topLevelElement, "global-tag-attributes"), globalAttributes, commonAttributes);
+        parseDynamicAttributes(getFirstChild(topLevelElement, "dynamic-tag-attributes"), dynamicAttributes, commonAttributes);
+        parseTagRules(getFirstChild(topLevelElement, "tag-rules"), commonAttributes, commonRegularExpressions, tagRules);
+        parseCSSRules(getFirstChild(topLevelElement, "css-rules"), cssRules, commonRegularExpressions);
+
+        parseAllowedEmptyTags(getFirstChild(topLevelElement, "allowed-empty-tags"), allowedEmptyTags);
+        parseRequireClosingTags(getFirstChild(topLevelElement, "require-closing-tags"), requireClosingTags);
+    }
+
+    /**
+     * Go through <directives> section of the policy file.
+     *
+     * @param root       Top level of <directives>
+     * @param directives The directives map to update
+     */
+    private static void parseDirectives(Element root, Map<String, String> directives) {
+        for (Element ele : getByTagName(root, "directive")) {
+            String name = getAttributeValue(ele, "name");
+            String value = getAttributeValue(ele, "value");
+            directives.put(name, value);
+        }
+    }
+
+    /**
+     * Go through <allowed-empty-tags> section of the policy file.
+     *
+     * @param allowedEmptyTagsListNode Top level of <allowed-empty-tags>
+     * @param allowedEmptyTags         The tags that can be empty
+     */
+    private static void parseAllowedEmptyTags(Element allowedEmptyTagsListNode,
+                                              List<String> allowedEmptyTags) throws PolicyException {
+        if (allowedEmptyTagsListNode != null) {
+            for (Element literalNode :
+                    getGrandChildrenByTagName(allowedEmptyTagsListNode, "literal-list", "literal")) {
+
+                String value = getAttributeValue(literalNode, "value");
+                if (value != null && value.length() > 0) {
+                    allowedEmptyTags.add(value);
+                }
+            }
+        } else allowedEmptyTags.addAll(Arrays.asList(
+                "br", "hr", "a", "img", "link", "iframe", "script", "object", "applet",
+                "frame", "base", "param", "meta", "input", "textarea", "embed",
+                "basefont", "col"));
+    }
+
+    /**
+     * Go through <require-closing-tags> section of the policy file.
+     *
+     * @param requireClosingTagsListNode Top level of <require-closing-tags>
+     * @param requireClosingTags         The list of tags that require closing
+     */
+    private static void parseRequireClosingTags(Element requireClosingTagsListNode,
+                                                List<String> requireClosingTags) throws PolicyException {
+        if (requireClosingTagsListNode != null) {
+            for (Element literalNode :
+                    getGrandChildrenByTagName(requireClosingTagsListNode, "literal-list", "literal")) {
+
+                String value = getAttributeValue(literalNode, "value");
+                if (value != null && value.length() > 0) {
+                    requireClosingTags.add(value);
+                }
+            }
+        } else requireClosingTags.addAll(Arrays.asList(
+                "iframe", "script", "link"
+        ));
+    }
+
+    /**
+     * Go through <global-tag-attributes> section of the policy file.
+     *
+     * @param root              Top level of <global-tag-attributes>
+     * @param globalAttributes1 A HashMap of global Attributes that need validation for every tag.
+     * @param commonAttributes  The common attributes
+     * @throws PolicyException
+     */
+    private static void parseGlobalAttributes(Element root, Map<String, Attribute> globalAttributes1, Map<String, Attribute> commonAttributes) throws PolicyException {
+        for (Element ele : getByTagName(root, "attribute")) {
+
+            String name = getAttributeValue(ele, "name");
+            Attribute toAdd = commonAttributes.get(name.toLowerCase());
+
+            if (toAdd != null) globalAttributes1.put(name.toLowerCase(), toAdd);
+            else throw new PolicyException("Global attribute '" + name
+                    + "' was not defined in <common-attributes>");
+        }
+    }
+
+    /**
+     * Go through <dynamic-tag-attributes> section of the policy file.
+     *
+     * @param root              Top level of <dynamic-tag-attributes>
+     * @param dynamicAttributes A HashMap of dynamic Attributes that need validation for every tag.
+     * @param commonAttributes  The common attributes
+     * @throws PolicyException
+     */
+    private static void parseDynamicAttributes(Element root, Map<String, Attribute> dynamicAttributes, Map<String, Attribute> commonAttributes) throws PolicyException {
+        for (Element ele : getByTagName(root, "attribute")) {
+
+            String name = getAttributeValue(ele, "name");
+            Attribute toAdd = commonAttributes.get(name.toLowerCase());
+
+            if (toAdd != null) {
+                String attrName = name.toLowerCase().substring(0, name.length() - 1);
+                dynamicAttributes.put(attrName, toAdd);
+            } else throw new PolicyException("Dynamic attribute '" + name
+                    + "' was not defined in <common-attributes>");
+        }
+    }
+
+    /**
+     * Go through the <common-regexps> section of the policy file.
+     *
+     * @param root                      Top level of <common-regexps>
+     * @param commonRegularExpressions1 the antisamy pattern objects
+     */
+    private static void parseCommonRegExps(Element root, Map<String, Pattern> commonRegularExpressions1) {
+        for (Element ele : getByTagName(root, "regexp")) {
+
+            String name = getAttributeValue(ele, "name");
+            Pattern pattern = Pattern.compile(getAttributeValue(ele, "value"), Pattern.DOTALL);
+            commonRegularExpressions1.put(name, pattern);
+        }
+    }
+
+    private static void parseCommonAttributes(Element root, Map<String, Attribute> commonAttributes1,
+                                              Map<String, Pattern> commonRegularExpressions1) {
+
+        for (Element ele : getByTagName(root, "attribute")) {
+            String onInvalid = getAttributeValue(ele, "onInvalid");
+            String name = getAttributeValue(ele, "name");
+
+            List<Pattern> allowedRegexps = getAllowedRegexps(commonRegularExpressions1, ele);
+            List<String> allowedValues = getAllowedLiterals(ele);
+
+            final String onInvalidStr;
+            if (onInvalid != null && onInvalid.length() > 0) {
+                onInvalidStr = onInvalid;
+            } else onInvalidStr =  "removeAttribute";
+
+            String description = getAttributeValue(ele, "description");
+            Attribute attribute = new Attribute(getAttributeValue(ele, "name"), allowedRegexps,
+                    allowedValues, onInvalidStr, description);
+            commonAttributes1.put(name.toLowerCase(), attribute);
+        }
+    }
+
+    private static List<String> getAllowedLiterals(Element ele) {
+        List<String> allowedValues = new ArrayList<String>();
+        for (Element literalNode : getGrandChildrenByTagName(ele, "literal-list", "literal")) {
+            String value = getAttributeValue(literalNode, "value");
+
+            if (value != null && value.length() > 0) {
+                allowedValues.add(value);
+            } else if (literalNode.getNodeValue() != null) {
+                allowedValues.add(literalNode.getNodeValue());
+            }
+        }
+        return allowedValues;
+    }
+
+    private static List<Pattern> getAllowedRegexps(Map<String, Pattern> commonRegularExpressions1, Element ele) {
+        List<Pattern> allowedRegExp = new ArrayList<Pattern>();
+        for (Element regExpNode : getGrandChildrenByTagName(ele, "regexp-list", "regexp")) {
+            String regExpName = getAttributeValue(regExpNode, "name");
+            String value = getAttributeValue(regExpNode, "value");
+
+            if (regExpName != null && regExpName.length() > 0) {
+                allowedRegExp.add(commonRegularExpressions1.get(regExpName));
+            } else allowedRegExp.add(Pattern.compile(value, Pattern.DOTALL));
+        }
+        return allowedRegExp;
+    }
+
+    private static List<Pattern> getAllowedRegexps2(Map<String, Pattern> commonRegularExpressions1,
+                                                    Element attributeNode, String tagName) throws PolicyException {
+        List<Pattern> allowedRegexps = new ArrayList<Pattern>();
+        for (Element regExpNode : getGrandChildrenByTagName(attributeNode, "regexp-list", "regexp")) {
+            String regExpName = getAttributeValue(regExpNode, "name");
+            String value = getAttributeValue(regExpNode, "value");
+
+            /*
+             * Look up common regular expression specified
+             * by the "name" field. They can put a common
+             * name in the "name" field or provide a custom
+             * value in the "value" field. They must choose
+             * one or the other, not both.
+             */
+            if (regExpName != null && regExpName.length() > 0) {
+                Pattern pattern = commonRegularExpressions1.get(regExpName);
+                if (pattern != null) {
+                    allowedRegexps.add(pattern);
+                } else throw new PolicyException("Regular expression '" + regExpName +
+                        "' was referenced as a common regexp in definition of '" + tagName +
+                        "', but does not exist in <common-regexp>");
+            } else if (value != null && value.length() > 0) {
+                allowedRegexps.add(Pattern.compile(value, Pattern.DOTALL));
+            }
+        }
+        return allowedRegexps;
+    }
+
+    private static List<Pattern> getAllowedRegexp3(Map<String, Pattern> commonRegularExpressions1,
+                                                   Element ele, String name) throws PolicyException {
+
+        List<Pattern> allowedRegExp = new ArrayList<Pattern>();
+        for (Element regExpNode : getGrandChildrenByTagName(ele, "regexp-list", "regexp")) {
+            String regExpName = getAttributeValue(regExpNode, "name");
+            String value = getAttributeValue(regExpNode, "value");
+
+            Pattern pattern = commonRegularExpressions1.get(regExpName);
+
+            if (pattern != null) {
+                allowedRegExp.add(pattern);
+            } else if (value != null) {
+                allowedRegExp.add(Pattern.compile(value, Pattern.DOTALL));
+            } else throw new PolicyException("Regular expression '" + regExpName +
+                    "' was referenced as a common regexp in definition of '" + name +
+                    "', but does not exist in <common-regexp>");
+        }
+        return allowedRegExp;
+    }
+
+    private static void parseTagRules(Element root, Map<String, Attribute> commonAttributes1, Map<String,
+            Pattern> commonRegularExpressions1, Map<String, Tag> tagRules1) throws PolicyException {
+
+        if (root == null) return;
+
+        for (Element tagNode : getByTagName(root, "tag")) {
+            String name = getAttributeValue(tagNode, "name");
+            String action = getAttributeValue(tagNode, "action");
+
+            NodeList attributeList = tagNode.getElementsByTagName("attribute");
+            Map<String, Attribute> tagAttributes = getTagAttributes(commonAttributes1, commonRegularExpressions1, attributeList, name);
+            Tag tag = new Tag(name, tagAttributes, action);
+
+            tagRules1.put(name.toLowerCase(), tag);
+        }
+    }
+
+    private static Map<String, Attribute> getTagAttributes(Map<String, Attribute> commonAttributes1, Map<String,
+            Pattern> commonRegularExpressions1, NodeList attributeList, String tagName) throws PolicyException {
+
+        Map<String,Attribute> tagAttributes = new HashMap<String, Attribute>();
+        for (int j = 0; j < attributeList.getLength(); j++) {
+            Element attributeNode = (Element) attributeList.item(j);
+
+            String attrName = getAttributeValue(attributeNode, "name").toLowerCase();
+            if (!attributeNode.hasChildNodes()) {
+                Attribute attribute = commonAttributes1.get(attrName);
+
+                // All they provided was the name, so they must want a common attribute.
+                if (attribute != null) {
+                    /*
+                     * If they provide onInvalid/description values here they will
+                     * override the common values.
+                     */
+
+                    String onInvalid = getAttributeValue(attributeNode, "onInvalid");
+                    String description = getAttributeValue(attributeNode, "description");
+                    Attribute changed = attribute.mutate(onInvalid, description);
+                    commonAttributes1.put(attrName, changed);
+                    tagAttributes.put(attrName, changed);
+
+                } else throw new PolicyException("Attribute '" + getAttributeValue(attributeNode, "name") +
+                        "' was referenced as a common attribute in definition of '" + tagName +
+                        "', but does not exist in <common-attributes>");
+
+            } else {
+                List<Pattern> allowedRegexps2 = getAllowedRegexps2(commonRegularExpressions1, attributeNode, tagName);
+                List<String> allowedValues2 = getAllowedLiterals(attributeNode);
+                String onInvalid = getAttributeValue(attributeNode, "onInvalid");
+                String description = getAttributeValue(attributeNode, "description");
+                Attribute attribute = new Attribute(getAttributeValue(attributeNode, "name"), allowedRegexps2, allowedValues2, onInvalid, description);
+
+                // Add fully built attribute.
+                tagAttributes.put(attrName, attribute);
+            }
+        }
+        return tagAttributes;
+    }
+
+    private static void parseCSSRules(Element root, Map<String, Property> cssRules1, Map<String, Pattern> commonRegularExpressions1) throws PolicyException {
+
+        for (Element ele : getByTagName(root, "property")) {
+            String name = getAttributeValue(ele, "name");
+            String description = getAttributeValue(ele, "description");
+
+            List<Pattern> allowedRegexp3 = getAllowedRegexp3(commonRegularExpressions1, ele, name);
+
+            List<String> allowedValue = new ArrayList<String>();
+            for (Element literalNode : getGrandChildrenByTagName(ele, "literal-list", "literal")) {
+                allowedValue.add(getAttributeValue(literalNode, "value"));
+            }
+
+            List<String> shortHandRefs = new ArrayList<String>();
+            for (Element shorthandNode : getGrandChildrenByTagName(ele, "shorthand-list", "shorthand")) {
+                shortHandRefs.add(getAttributeValue(shorthandNode, "name"));
+            }
+
+            String onInvalid = getAttributeValue(ele, "onInvalid");
+            final String onInvalidStr;
+            if (onInvalid != null && onInvalid.length() > 0) {
+                onInvalidStr = onInvalid;
+            } else onInvalidStr =  "removeAttribute";
+
+            Property property = new Property(name,allowedRegexp3, allowedValue, shortHandRefs, description, onInvalidStr);
+            cssRules1.put(name.toLowerCase(), property);
+        }
+    }
+
+    private static Element getFirstChild(Element element, String tagName) {
+        if (element == null) return null;
+        NodeList elementsByTagName = element.getElementsByTagName(tagName);
+        if (elementsByTagName != null && elementsByTagName.getLength() > 0)
+            return (Element) elementsByTagName.item(0);
+        else return null;
+    }
+
+    private static Iterable<Element>  getGrandChildrenByTagName(Element parent, String immediateChildName, String subChild){
+        NodeList elementsByTagName = parent.getElementsByTagName(immediateChildName);
+        if (elementsByTagName.getLength() == 0) return Collections.emptyList();
+        Element regExpListNode = (Element) elementsByTagName.item(0);
+        return getByTagName( regExpListNode, subChild);
+    }
+
+    private static Iterable<Element> getByTagName(Element parent, String tagName) {
+        if (parent == null) return Collections.emptyList();
+
+        final NodeList nodes = parent.getElementsByTagName(tagName);
+        return new Iterable<Element>() {
+            public Iterator<Element> iterator() {
+                return new Iterator<Element>() {
+                    int pos = 0;
+                    int len = nodes.getLength();
+
+                    public boolean hasNext() {
+                        return pos < len;
+                    }
+
+                    public Element next() {
+                        return (Element) nodes.item(pos++);
+                    }
+
+                    public void remove() {
+                        throw new UnsupportedOperationException("Cant remove");
+                    }
+                };
+            }
+        };
+    }
+
+    static class SAXErrorHandler implements ErrorHandler {
+        @Override
+        public void error(SAXParseException arg0) throws SAXException {
+            throw arg0;
+        }
+
+        @Override
+        public void fatalError(SAXParseException arg0) throws SAXException {
+            throw arg0;
+        }
+
+        @Override
+        public void warning(SAXParseException arg0) throws SAXException {
+            throw arg0;
+        }
+    }
+}
diff --git a/src/main/java/org/owasp/validator/html/PolicyException.java b/src/main/java/org/owasp/validator/html/PolicyException.java
new file mode 100644
index 0000000..d58a008
--- /dev/null
+++ b/src/main/java/org/owasp/validator/html/PolicyException.java
@@ -0,0 +1,9 @@
+package org.owasp.validator.html;
+
+public class PolicyException extends Exception {
+    public PolicyException(Exception e) {
+    }
+
+    public PolicyException(String s) {
+    }
+}
diff --git a/src/main/java/org/owasp/validator/html/ScanException.java b/src/main/java/org/owasp/validator/html/ScanException.java
new file mode 100644
index 0000000..0b60bed
--- /dev/null
+++ b/src/main/java/org/owasp/validator/html/ScanException.java
@@ -0,0 +1,4 @@
+package org.owasp.validator.html;
+
+public class ScanException extends Exception {
+}
diff --git a/src/main/java/org/owasp/validator/html/model/Attribute.java b/src/main/java/org/owasp/validator/html/model/Attribute.java
new file mode 100644
index 0000000..56536f0
--- /dev/null
+++ b/src/main/java/org/owasp/validator/html/model/Attribute.java
@@ -0,0 +1,39 @@
+package org.owasp.validator.html.model;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class Attribute {
+
+    private final String name;
+    private final List<Pattern> allowedRegexps;
+    private final List<String> allowedValues;
+    private final String onInvalidStr;
+    private final String description;
+
+    public Attribute(String name, List<Pattern> allowedRegexps, List<String> allowedValues, String onInvalidStr, String description) {
+        this.name = name;
+        this.allowedRegexps = allowedRegexps;
+        this.allowedValues = allowedValues;
+        this.onInvalidStr = onInvalidStr;
+        this.description = description;
+    }
+
+    public boolean matchesAllowedExpression(String value){
+        String input = value.toLowerCase();
+        for (Pattern pattern : allowedRegexps) {
+            if (pattern != null && pattern.matcher(input).matches()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean containsAllowedValue(String valueInLowerCase){
+        return allowedValues.contains(valueInLowerCase);
+    }
+
+    public Attribute mutate(String onInvalid, String description) {
+        return new Attribute(name, allowedRegexps, allowedValues, onInvalidStr, description);
+    }
+}
diff --git a/src/main/java/org/owasp/validator/html/model/Property.java b/src/main/java/org/owasp/validator/html/model/Property.java
new file mode 100644
index 0000000..bd2fa5c
--- /dev/null
+++ b/src/main/java/org/owasp/validator/html/model/Property.java
@@ -0,0 +1,22 @@
+package org.owasp.validator.html.model;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class Property {
+    private final String name;
+    private final List<Pattern> allowedRegexp3;
+    private final List<String> allowedValue;
+    private final List<String> shortHandRefs;
+    private final String description;
+    private final String onInvalidStr;
+
+    public Property(String name, List<Pattern> allowedRegexp3, List<String> allowedValue, List<String> shortHandRefs, String description, String onInvalidStr) {
+        this.name = name;
+        this.allowedRegexp3 = allowedRegexp3;
+        this.allowedValue = allowedValue;
+        this.shortHandRefs = shortHandRefs;
+        this.description = description;
+        this.onInvalidStr = onInvalidStr;
+    }
+}
diff --git a/src/main/java/org/owasp/validator/html/model/Tag.java b/src/main/java/org/owasp/validator/html/model/Tag.java
new file mode 100644
index 0000000..a9c1df5
--- /dev/null
+++ b/src/main/java/org/owasp/validator/html/model/Tag.java
@@ -0,0 +1,41 @@
+package org.owasp.validator.html.model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class Tag {
+
+    private final String name;
+    private final Map<String, Attribute> tagAttributes;
+    private final String action;
+
+    public Tag(String name, Map<String, Attribute> tagAttributes, String action) {
+        this.name = name;
+        this.tagAttributes = tagAttributes;
+        this.action = action;
+    }
+
+    public Attribute getAttributeByName(String href) {
+        return tagAttributes.get(href);
+    }
+
+    public String getAction() {
+        throw new IllegalStateException();
+    }
+
+    public boolean isAction(String action) {
+        throw new IllegalStateException();
+    }
+
+    public Tag mutateAction(String action) {
+        return new Tag(name, tagAttributes, action);
+    }
+
+    public String getRegularExpression() {
+        throw new IllegalStateException();
+    }
+
+    public String getName() {
+        throw new IllegalStateException();
+    }
+}
diff --git a/src/main/java/org/owasp/validator/html/util/XMLUtil.java b/src/main/java/org/owasp/validator/html/util/XMLUtil.java
new file mode 100644
index 0000000..4306487
--- /dev/null
+++ b/src/main/java/org/owasp/validator/html/util/XMLUtil.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) 2007-2019, Arshan Dabirsiaghi, Jason Li
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ * Neither the name of OWASP nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.owasp.validator.html.util;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class XMLUtil {
+
+    private static final Pattern encgt = Pattern.compile("&gt;");
+    private static final Pattern enclt = Pattern.compile("&lt;");
+    private static final Pattern encQuot = Pattern.compile("&quot;");
+    private static final Pattern encAmp = Pattern.compile("&amp;");
+
+    private static final Pattern gt = Pattern.compile(">");
+    private static final Pattern lt = Pattern.compile("<");
+    private static final Pattern quot = Pattern.compile("\"");
+    private static final Pattern amp = Pattern.compile("&");
+
+    /**
+     * Helper function for quickly retrieving an attribute from a given
+     * element.
+     * @param ele The document element from which to pull the attribute value.
+     * @param attrName The name of the attribute.
+     * @return The value of the attribute contained within the element
+     */
+    public static String getAttributeValue (Element ele, String attrName) {
+        return decode(ele.getAttribute(attrName));
+    }
+
+    /**
+     * Helper function for quickly retrieving an integer value of a given
+     * XML element.
+     * @param ele The document element from which to pull the integer value.
+     * @param tagName The name of the node.
+     * @param defaultValue The default int to return if the specified XML element cannot be found
+     *           or parsed properly.
+     * @return The integer value of the given node in the element passed in.
+     */
+
+    public static int getIntValue(Element ele, String tagName, int defaultValue) {
+
+        int toReturn = defaultValue;
+
+        try {
+            toReturn = Integer.parseInt(getTextValue(ele,tagName));
+        } catch (Throwable t) { }
+
+        return toReturn;
+    }
+
+
+    /**
+     * Helper function for quickly retrieving a String value of a given
+     * XML element.
+     * @param ele The document element from which to pull the String value.
+     * @param tagName The name of the node.
+     * @return The String value of the given node in the element passed in.
+     */
+    public static String getTextValue(Element ele, String tagName) {
+        String textVal = null;
+        NodeList nl = ele.getElementsByTagName(tagName);
+        if(nl != null && nl.getLength() > 0) {
+            Element el = (Element)nl.item(0);
+            if ( el.getFirstChild() != null ) {
+                textVal = el.getFirstChild().getNodeValue();
+            } else {
+                textVal = "";
+            }
+        }
+        return decode(textVal);
+    }
+
+
+    /**
+     * Helper function for quickly retrieving an boolean value of a given
+     * XML element.
+     * @param ele The document element from which to pull the boolean value.
+     * @param tagName The name of the node.
+     * @return The boolean value of the given node in the element passed in.
+     */
+    public static boolean getBooleanValue(Element ele, String tagName) {
+
+        boolean boolVal = false;
+        NodeList nl = ele.getElementsByTagName(tagName);
+
+        if ( nl != null && nl.getLength() > 0 ) {
+            Element el = (Element)nl.item(0);
+            boolVal = el.getFirstChild().getNodeValue().equals("true");
+        }
+
+        return boolVal;
+    }
+
+    /**
+     * Helper function for quickly retrieving an boolean value of a given
+     * XML element, with a default initialization value passed in a parameter.
+     * @param ele The document element from which to pull the boolean value.
+     * @param tagName The name of the node.
+     * @param defaultValue The default value of the node if it's value can't be processed.
+     * @return The boolean value of the given node in the element passed in.
+     */
+    public static boolean getBooleanValue(Element ele, String tagName, boolean defaultValue) {
+
+        boolean boolVal = defaultValue;
+        NodeList nl = ele.getElementsByTagName(tagName);
+
+        if ( nl != null && nl.getLength() > 0 ) {
+
+            Element el = (Element)nl.item(0);
+
+            if ( el.getFirstChild().getNodeValue() != null ) {
+
+                boolVal = "true".equals(el.getFirstChild().getNodeValue());
+
+            } else {
+
+                boolVal = defaultValue;
+
+            }
+        }
+
+        return boolVal;
+    }
+
+
+    /**
+     * Helper function for decode XML entities.
+     *
+     * @param str The XML-encoded String to decode.
+     * @return An XML-decoded String.
+     */
+    public static String decode(String str) {
+
+        if (str == null) {
+            return null;
+        }
+
+        Matcher gtmatcher = encgt.matcher(str);
+        if (gtmatcher.matches()) {
+            str = gtmatcher.replaceAll(">");
+        }
+        Matcher ltmatcher = enclt.matcher(str);
+        if (ltmatcher.matches()) {
+            str = ltmatcher.replaceAll("<");
+        }
+        Matcher quotMatcher = encQuot.matcher(str);
+        if (quotMatcher.matches()) {
+            str = quotMatcher.replaceAll("\"");
+        }
+        Matcher ampMatcher = encAmp.matcher(str);
+        if (ampMatcher.matches()) {
+            str = ampMatcher.replaceAll("&");
+        }
+
+        return str;
+    }
+
+    public static String encode(String str) {
+
+        if (str == null) {
+            return null;
+        }
+
+        Matcher gtMatcher = gt.matcher(str);
+        if (gtMatcher.matches()) {
+            str = gtMatcher.replaceAll("&gt;");
+        }
+        Matcher ltMatcher = lt.matcher(str);
+        if (ltMatcher.matches()) {
+            str = ltMatcher.replaceAll("&lt;");
+        }
+        Matcher quotMatcher = quot.matcher(str);
+        if (quotMatcher.matches()) {
+            str = quotMatcher.replaceAll("&quot;");
+        }
+        Matcher ampMatcher = amp.matcher(str);
+        if (ampMatcher.matches()) {
+            str = ampMatcher.replaceAll("&amp;");
+        }
+
+        return str;
+    }
+}
\ No newline at end of file
diff --git a/src/main/resources/antisamy.xsd b/src/main/resources/antisamy.xsd
new file mode 100644
index 0000000..2ddec21
--- /dev/null
+++ b/src/main/resources/antisamy.xsd
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+
+    <xsd:element name="anti-samy-rules">
+        <xsd:complexType>
+            <xsd:sequence>
+                <xsd:element name="include" type="Include" minOccurs="0" maxOccurs="unbounded"/>
+                <xsd:element name="directives" type="Directives"/>
+                <xsd:element name="common-regexps" type="CommonRegexps"/>
+                <xsd:element name="common-attributes" type="AttributeList"/>
+                <xsd:element name="global-tag-attributes" type="AttributeList"/>
+                <xsd:element name="dynamic-tag-attributes" type="AttributeList" minOccurs="0"/>
+                <xsd:element name="tags-to-encode" type="TagsToEncodeList" minOccurs="0"/>
+                <xsd:element name="tag-rules" type="TagRules"/>
+                <xsd:element name="css-rules" type="CSSRules"/>
+                <xsd:element name="allowed-empty-tags" type="AllowedEmptyTags" minOccurs="0"/>
+            </xsd:sequence>
+        </xsd:complexType>
+    </xsd:element>
+
+    <xsd:complexType name="Include">
+        <xsd:attribute name="href" use="required" type="xsd:string"/>
+    </xsd:complexType>
+
+    <xsd:complexType name="Directives">
+        <xsd:sequence maxOccurs="unbounded">
+            <xsd:element name="directive" type="Directive" minOccurs="0"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="Directive">
+        <xsd:attribute name="name" use="required"/>
+        <xsd:attribute name="value" use="required"/>
+    </xsd:complexType>
+
+    <xsd:complexType name="CommonRegexps">
+        <xsd:sequence maxOccurs="unbounded">
+            <xsd:element name="regexp" type="RegExp" minOccurs="0"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="AttributeList">
+        <xsd:sequence maxOccurs="unbounded">
+            <xsd:element name="attribute" type="Attribute" minOccurs="0"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="TagsToEncodeList">
+        <xsd:sequence maxOccurs="unbounded">
+            <xsd:element name="tag" minOccurs="0"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="TagRules">
+        <xsd:sequence maxOccurs="unbounded">
+            <xsd:element name="tag" type="Tag" minOccurs="0"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="Tag">
+        <xsd:sequence maxOccurs="unbounded">
+            <xsd:element name="attribute" type="Attribute" minOccurs="0" />
+        </xsd:sequence>
+        <xsd:attribute name="name" use="required"/>
+        <xsd:attribute name="action" use="required"/>
+    </xsd:complexType>
+
+    <xsd:complexType name="AllowedEmptyTags">
+        <xsd:sequence>
+            <xsd:element name="literal-list" type="LiteralList" minOccurs="0"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="Attribute">
+        <xsd:sequence>
+            <xsd:element name="regexp-list" type="RegexpList" minOccurs="0"/>
+            <xsd:element name="literal-list" type="LiteralList" minOccurs="0"/>
+        </xsd:sequence>
+        <xsd:attribute name="name" use="required"/>
+        <xsd:attribute name="description"/>
+        <xsd:attribute name="onInvalid"/>
+    </xsd:complexType>
+
+    <xsd:complexType name="RegexpList">
+        <xsd:sequence maxOccurs="unbounded">
+            <xsd:element name="regexp" type="RegExp" minOccurs="0"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="RegExp">
+        <xsd:attribute name="name" type="xsd:string"/>
+        <xsd:attribute name="value" type="xsd:string"/>
+    </xsd:complexType>
+
+    <xsd:complexType name="LiteralList">
+        <xsd:sequence maxOccurs="unbounded">
+            <xsd:element name="literal" type="Literal" minOccurs="0"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="Literal">
+        <xsd:attribute name="value" type="xsd:string"/>
+    </xsd:complexType>
+
+    <xsd:complexType name="CSSRules">
+        <xsd:sequence maxOccurs="unbounded">
+            <xsd:element name="property" type="Property" minOccurs="0"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="Property">
+        <xsd:sequence>
+            <xsd:element name="category-list" type="CategoryList" minOccurs="0"/>
+            <xsd:element name="literal-list" type="LiteralList" minOccurs="0"/>
+            <xsd:element name="regexp-list" type="RegexpList" minOccurs="0"/>
+            <xsd:element name="shorthand-list" type="ShorthandList" minOccurs="0"/>
+        </xsd:sequence>
+        <xsd:attribute name="name" type="xsd:string" use="required"/>
+        <xsd:attribute name="default" type="xsd:string"/>
+        <xsd:attribute name="description" type="xsd:string"/>
+    </xsd:complexType>
+
+    <xsd:complexType name="ShorthandList">
+        <xsd:sequence maxOccurs="unbounded">
+            <xsd:element name="shorthand" type="Shorthand" minOccurs="0"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="Shorthand">
+        <xsd:attribute name="name" type="xsd:string" use="required"/>
+    </xsd:complexType>
+
+    <xsd:complexType name="CategoryList">
+        <xsd:sequence maxOccurs="unbounded">
+            <xsd:element name="category" type="Category" minOccurs="0"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="Category">
+        <xsd:attribute name="value" type="xsd:string" use="required"/>
+    </xsd:complexType>
+
+    <xsd:complexType name="Entity">
+        <xsd:attribute name="name" type="xsd:string" use="required"/>
+        <xsd:attribute name="cdata" type="xsd:string" use="required"/>
+    </xsd:complexType>
+</xsd:schema>
\ No newline at end of file