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}\\.\\#@\\$%\\+&;:\\-_~,\\?=/!\\*\\(\\)]*(\\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(">");
+ private static final Pattern enclt = Pattern.compile("<");
+ private static final Pattern encQuot = Pattern.compile(""");
+ private static final Pattern encAmp = Pattern.compile("&");
+
+ 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(">");
+ }
+ Matcher ltMatcher = lt.matcher(str);
+ if (ltMatcher.matches()) {
+ str = ltMatcher.replaceAll("<");
+ }
+ Matcher quotMatcher = quot.matcher(str);
+ if (quotMatcher.matches()) {
+ str = quotMatcher.replaceAll(""");
+ }
+ Matcher ampMatcher = amp.matcher(str);
+ if (ampMatcher.matches()) {
+ str = ampMatcher.replaceAll("&");
+ }
+
+ 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