Skip to content

Commit

Permalink
Script type importmap (#1478)
Browse files Browse the repository at this point in the history
Fixes #1309
Fixes #1437
  • Loading branch information
NiedziolkaMichal committed Nov 30, 2022
1 parent 5d18730 commit 4054c56
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 1 deletion.
2 changes: 1 addition & 1 deletion schema/html5/core-scripting.rnc
Expand Up @@ -50,7 +50,7 @@ datatypes w = "http://whattf.org/datatype-draft"
} & v5only
script.attrs.type =
attribute type {
w:string "module" | w:string "" | common.data.scripttype # XXX without charset parameter!
w:string "module" | w:string "importmap" | w:string "" | common.data.scripttype # XXX without charset parameter!
}
script.attrs.nomodule =
attribute nomodule {
Expand Down
152 changes: 152 additions & 0 deletions src/nu/validator/checker/schematronequiv/Assertions.java
Expand Up @@ -39,6 +39,8 @@

import javax.servlet.http.HttpServletRequest;

import io.mola.galimatias.GalimatiasParseException;
import io.mola.galimatias.URL;
import nu.validator.checker.AttributeUtil;
import nu.validator.checker.Checker;
import nu.validator.checker.LocatorImpl;
Expand All @@ -64,6 +66,7 @@
import nu.validator.htmlparser.impl.NCName;
import nu.validator.messages.MessageEmitterAdapter;

import org.eclipse.jetty.util.ajax.JSON;
import org.relaxng.datatype.DatatypeException;

import org.w3c.css.css.StyleSheetParser;
Expand Down Expand Up @@ -1337,6 +1340,8 @@ private StackNode peek() {
private boolean hasTopLevelH1;

private boolean hasAncestorTableIsRoleTableGridOrTreeGrid = false;

private boolean parsingScriptImportMap = false;

private int numberOfTemplatesDeep = 0;

Expand Down Expand Up @@ -1690,6 +1695,9 @@ public void endElement(String uri, String localName, String name)
|| "nav" == localName || "section" == localName) {
sectioningElementPtrs.pollLast();
}
if ("script" == localName && parsingScriptImportMap) {
isImportMapValid(node.getTextContent().toString());
}
}
if ((locator = openActiveDescendants.remove(node)) != null) {
warn("Attribute \u201Caria-activedescendant\u201D value should "
Expand All @@ -1699,6 +1707,142 @@ public void endElement(String uri, String localName, String name)
}
}

private boolean isImportMapValid(String scriptContent) throws SAXException {
Object importMap;
try {
importMap = JSON.parse(scriptContent, true);
} catch (IllegalStateException e) {
err("A script \u201cscript\u201d with a \u201ctype\u201d attribute"
+ " whose value is \u201cimportmap\u201d must have valid"
+ " JSON content.");
return false;
}
if (!(importMap instanceof Map)) {
err("A \u201cscript\u201d element with a \u201ctype\u201d attribute"
+ " whose value is \u201cimportmap\u201d must contain a"
+ " JSON object.");
return false;
}

Map<String, Object> importMapObject = (Map<String, Object>) importMap;

for (Map.Entry<String, Object> importMapEntry : importMapObject.entrySet()) {
String specifierType = importMapEntry.getKey();
if (!"imports".equals(specifierType)
&& !"scopes".equals(specifierType)) {
err("A \u201cscript\u201d element with a \u201ctype\u201d"
+ " attribute whose value is \u201cimportmap\u201d must"
+ " contain a JSON object with no properties other than"
+ " \u201cimports\u201d and \u201cscopes\u201d.");
return false;
}
if (!(importMapEntry.getValue() instanceof Map)) {
err("The value of the \u201c" + specifierType + "\u201d"
+ " property within the content of a \u201cscript\u201d"
+ " element with a \u201ctype\u201d attribute whose"
+ " value is \u201cimportmap\u201d must be a JSON"
+ " object.");
return false;
}

Map<String, Object> importMapValue = (Map<String, Object>) importMapEntry.getValue();
for (Map.Entry<String, Object> entry : importMapValue.entrySet()) {

if ("imports".equals(specifierType)) {
if (!isSpecifierMapValid(specifierType, entry.getKey(),
entry.getValue())) {
return false;
}
} else if ("scopes".equals(specifierType)) {
if (!isValidURL(entry.getKey())) {
err("The value of the \u201cscopes\u201d property"
+ " within the content of a \u201cscript\u201d"
+ " element with a \u201ctype\u201d attribute"
+ " whose value is \u201cimportmap\u201d must"
+ " be a JSON object whose keys are valid URL"
+ " strings.");
return false;
}
if (!(entry.getValue() instanceof Map)) {
err("The value of the \u201cscopes\u201d property"
+ " within the content of a \u201cscript\u201d"
+ " element with a \u201ctype\u201d attribute"
+ " whose value is \u201cimportmap\u201d must"
+ " be a JSON object whose values are also"
+ " JSON objects.");
return false;
}
Map<String, Object> scopesMap = (Map<String, Object>) entry.getValue();
for (Map.Entry<String, Object> scopesEntry : scopesMap.entrySet()) {
if (!isSpecifierMapValid(specifierType,
scopesEntry.getKey(), scopesEntry.getValue())) {
return false;
}
}
}
}
}
return true;
}

private boolean isSpecifierMapValid(String type, String key, Object value)
throws SAXException {
if (key.isEmpty()) {
err("A specifier map defined in a \u201c" + type + "\u201d"
+ " property within the content of a \u201cscript\u201d"
+ " element with a \u201ctype\u201d attribute whose value"
+ " is \u201cimportmap\u201d must only contain non-empty"
+ " keys.");
return false;
}
if (!(value instanceof String)) {
err("A specifier map defined in a \u201c" + type + "\u201d"
+ " property within the content of a \u201cscript\u201d"
+ " element with a \u201ctype\u201d attribute whose value"
+ " is \u201cimportmap\u201d must only contain string"
+ " values.");
return false;
}
String sValue = (String) value;
if (!isValidURL(sValue)) {
err("A specifier map defined in a \u201c" + type + "\u201d"
+ " property within the content of a \u201cscript\u201d"
+ " element with a \u201ctype\u201d attribute whose value"
+ " is \u201cimportmap\u201d must only contain valid URL"
+ " values.");
return false;
}
if (key.endsWith("/") && !sValue.endsWith("/")) {
err("A specifier map defined in a \u201c" + type + "\u201d"
+ " property within the content of a \u201cscript\u201d"
+ " element with a \u201ctype\u201d attribute whose value"
+ " is \u201cimportmap\u201d must have values that end with"
+ " \u201c/\u201d when its corresponding key ends with"
+ " \u201c/\u201d.");
return false;
}
return true;
}

private boolean isValidURL(String value) {
try {
URL.parse(value);
return true;
} catch (GalimatiasParseException e) {
}

if (value.startsWith("/") || value.startsWith("./")
|| value.startsWith("../")) {
try {
URL.parse("https://example.com/" + value);
return true;
} catch (GalimatiasParseException e) {
}
}

return false;
}

/**
* @see nu.validator.checker.Checker#startDocument()
*/
Expand Down Expand Up @@ -2783,6 +2927,14 @@ else if ("script" == localName) {
+ " \u201Ctype\u201D attribute with the value"
+ " \u201Cmodule\u201D.");
}
} else if ("importmap".equals(scriptType)) {
if (atts.getIndex("", "src") > -1) {
err("A \u201cscript\u201d element with a"
+ " \u201ctype\u201d attribute whose value"
+ " is \u201cimportmap\u201d must not have"
+ " a \u201Csrc\u201D attribute.");
}
parsingScriptImportMap = true;
}
}
}
Expand Down

0 comments on commit 4054c56

Please sign in to comment.