@@ -0,0 +1,9 @@ | |||
*.iml | |||
.idea | |||
target | |||
tmp | |||
logs | |||
dependency-reduced-pom.xml | |||
velocity.log | |||
*~ | |||
build.log |
@@ -0,0 +1,53 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!-- | |||
(c) Copyright 2019 Jonathan Cobb | |||
This code is available under the Apache License, version 2: http://www.apache.org/licenses/LICENSE-2.0.html | |||
--> | |||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |||
<modelVersion>4.0.0</modelVersion> | |||
<groupId>bubble</groupId> | |||
<artifactId>abp-parser</artifactId> | |||
<version>1.0.0-SNAPSHOT</version> | |||
<packaging>jar</packaging> | |||
<licenses> | |||
<license> | |||
<name>The Apache Software License, Version 2.0</name> | |||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> | |||
<distribution>repo</distribution> | |||
</license> | |||
</licenses> | |||
<dependencies> | |||
<dependency> | |||
<groupId>org.cobbzilla</groupId> | |||
<artifactId>cobbzilla-utils</artifactId> | |||
<version>1.0.0-SNAPSHOT</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>junit</groupId> | |||
<artifactId>junit</artifactId> | |||
<version>4.12</version> | |||
<scope>test</scope> | |||
</dependency> | |||
</dependencies> | |||
<build> | |||
<plugins> | |||
<plugin> | |||
<groupId>org.apache.maven.plugins</groupId> | |||
<artifactId>maven-compiler-plugin</artifactId> | |||
<version>2.3.2</version> | |||
<configuration> | |||
<source>11</source> | |||
<target>11</target> | |||
<showWarnings>true</showWarnings> | |||
</configuration> | |||
</plugin> | |||
</plugins> | |||
</build> | |||
</project> |
@@ -0,0 +1,29 @@ | |||
package bubble.abp.spec; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.Setter; | |||
import lombok.experimental.Accessors; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
@NoArgsConstructor @Accessors(chain=true) | |||
public class BlockDecision { | |||
public static final BlockDecision BLOCK = new BlockDecision().setDecisionType(BlockDecisionType.block); | |||
public static final BlockDecision ALLOW = new BlockDecision().setDecisionType(BlockDecisionType.allow); | |||
@Getter @Setter BlockDecisionType decisionType = BlockDecisionType.allow; | |||
@Getter @Setter List<BlockSpec> specs; | |||
public BlockDecision add(BlockSpec spec) { | |||
if (specs == null) specs = new ArrayList<>(); | |||
specs.add(spec); | |||
if (decisionType != BlockDecisionType.block && (spec.hasTypeMatches() || spec.hasSelector())) { | |||
decisionType = BlockDecisionType.filter; | |||
} | |||
return this; | |||
} | |||
} |
@@ -0,0 +1,11 @@ | |||
package bubble.abp.spec; | |||
import com.fasterxml.jackson.annotation.JsonCreator; | |||
public enum BlockDecisionType { | |||
block, allow, filter; | |||
@JsonCreator public static BlockDecisionType fromString (String v) { return valueOf(v.toLowerCase()); } | |||
} |
@@ -0,0 +1,57 @@ | |||
package bubble.abp.spec; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.experimental.Accessors; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.util.*; | |||
@NoArgsConstructor @Accessors(chain=true) @Slf4j | |||
public class BlockList { | |||
@Getter private Set<BlockSpec> blacklist = new HashSet<>(); | |||
@Getter private Set<BlockSpec> whitelist = new HashSet<>(); | |||
public void addToWhitelist(BlockSpec spec) { | |||
whitelist.add(spec); | |||
} | |||
public void addToWhitelist(List<BlockSpec> specs) { | |||
for (BlockSpec spec : specs) addToWhitelist(spec); | |||
} | |||
public void addToBlacklist(BlockSpec spec) { | |||
blacklist.add(spec); | |||
} | |||
public void addToBlacklist(List<BlockSpec> specs) { | |||
for (BlockSpec spec : specs) addToBlacklist(spec); | |||
} | |||
public void merge(BlockList other) { | |||
for (BlockSpec allow : other.getWhitelist()) { | |||
addToWhitelist(allow); | |||
} | |||
for (BlockSpec block : other.getBlacklist()) { | |||
addToBlacklist(block); | |||
} | |||
} | |||
public BlockDecision getDecision(String fqdn, String path) { | |||
for (BlockSpec allow : whitelist) { | |||
if (allow.matches(fqdn, path)) return BlockDecision.ALLOW; | |||
} | |||
final BlockDecision decision = new BlockDecision(); | |||
for (BlockSpec block : blacklist) { | |||
if (block.matches(fqdn, path)) { | |||
if (!block.hasSelector()) return BlockDecision.BLOCK; | |||
decision.add(block); | |||
} else if (block.hasSelector()) { | |||
decision.add(block); | |||
} | |||
} | |||
return decision; | |||
} | |||
} |
@@ -0,0 +1,55 @@ | |||
package bubble.abp.spec; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.Setter; | |||
import lombok.experimental.Accessors; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.http.HttpUtil; | |||
import java.io.BufferedReader; | |||
import java.io.IOException; | |||
import java.io.InputStreamReader; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.now; | |||
@NoArgsConstructor @Accessors(chain=true) @Slf4j | |||
public class BlockListSource { | |||
@Getter @Setter private String url; | |||
@Getter @Setter private String format; | |||
@Getter @Setter private Long lastDownloaded; | |||
public long age () { return lastDownloaded == null ? Long.MAX_VALUE : now() - lastDownloaded; } | |||
@Getter @Setter private BlockList blockList = new BlockList(); | |||
public BlockListSource download() throws IOException { | |||
try (BufferedReader r = new BufferedReader(new InputStreamReader(HttpUtil.get(url)))) { | |||
String line; | |||
boolean firstLine = true; | |||
while ( (line = r.readLine()) != null ) { | |||
if (empty(line)) continue; | |||
line = line.trim(); | |||
if (firstLine && line.startsWith("[") && line.endsWith("]")) { | |||
format = line.substring(1, line.length()-1); | |||
} | |||
firstLine = false; | |||
if (line.startsWith("!")) continue; | |||
try { | |||
if (line.startsWith("@@")) { | |||
blockList.addToWhitelist(BlockSpec.parse(line)); | |||
} else { | |||
blockList.addToBlacklist(BlockSpec.parse(line)); | |||
} | |||
} catch (Exception e) { | |||
log.warn("download("+url+"): error parsing line (skipping due to "+shortError(e)+"): " + line); | |||
} | |||
} | |||
} | |||
lastDownloaded = now(); | |||
return this; | |||
} | |||
} |
@@ -0,0 +1,179 @@ | |||
package bubble.abp.spec; | |||
import lombok.EqualsAndHashCode; | |||
import lombok.Getter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.http.HttpContentTypes; | |||
import org.cobbzilla.util.string.StringUtil; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.http.HttpContentTypes.contentType; | |||
@Slf4j @EqualsAndHashCode(of={"target", "domainExclusions", "typeMatches", "typeExclusions", "selector"}) | |||
public class BlockSpec { | |||
public static final String OPT_DOMAIN_PREFIX = "domain="; | |||
public static final String OPT_SCRIPT = "script"; | |||
public static final String OPT_IMAGE = "image"; | |||
public static final String OPT_STYLESHEET = "stylesheet"; | |||
@Getter private String line; | |||
@Getter private BlockSpecTarget target; | |||
@Getter private List<String> domainExclusions; | |||
@Getter private List<String> typeMatches; | |||
public boolean hasTypeMatches () { return !empty(typeMatches); } | |||
@Getter private List<String> typeExclusions; | |||
@Getter private String selector; | |||
public boolean hasSelector() { return !empty(selector); } | |||
public BlockSpec(String line, BlockSpecTarget target, List<String> options, String selector) { | |||
this.line = line; | |||
this.target = target; | |||
this.selector = selector; | |||
if (options != null) { | |||
for (String opt : options) { | |||
if (opt.startsWith(OPT_DOMAIN_PREFIX)) { | |||
processDomainOptions(opt.substring(OPT_DOMAIN_PREFIX.length())); | |||
} else if (opt.startsWith("~")) { | |||
final String type = opt.substring(1); | |||
if (isTypeOption(type)) { | |||
if (typeExclusions == null) typeExclusions = new ArrayList<>(); | |||
typeExclusions.add(type); | |||
} else { | |||
log.warn("unsupported option (ignoring): " + opt); | |||
} | |||
} else { | |||
if (isTypeOption(opt)) { | |||
if (typeMatches == null) typeMatches = new ArrayList<>(); | |||
typeMatches.add(opt); | |||
} else { | |||
log.warn("unsupported option (ignoring): "+opt); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
private void processDomainOptions(String option) { | |||
final String[] parts = option.split("\\|"); | |||
for (String domainOption : parts) { | |||
if (domainOption.startsWith("~")) { | |||
if (domainExclusions == null) domainExclusions = new ArrayList<>(); | |||
domainExclusions.add(domainOption.substring(1)); | |||
} else { | |||
log.warn("ignoring included domain: "+domainOption); | |||
} | |||
} | |||
} | |||
public boolean isTypeOption(String type) { | |||
return type.equals(OPT_SCRIPT) || type.equals(OPT_IMAGE) || type.equals(OPT_STYLESHEET); | |||
} | |||
public static List<BlockSpec> parse(String line) { | |||
line = line.trim(); | |||
int optionStartPos = line.indexOf('$'); | |||
int selectorStartPos = line.indexOf("#"); | |||
// sanity check that selectorStartPos > optionStartPos -- $ may occur AFTER ## if the selector contains a regex | |||
if (selectorStartPos != -1 && optionStartPos > selectorStartPos) optionStartPos = -1; | |||
final List<BlockSpecTarget> targets; | |||
final List<String> options; | |||
final String selector; | |||
if (optionStartPos == -1) { | |||
if (selectorStartPos == -1) { | |||
// no options, no selector, entire line is the target | |||
targets = BlockSpecTarget.parseBareLine(line); | |||
options = null; | |||
selector = null; | |||
} else { | |||
// no options, but selector present. split into target + selector | |||
targets = BlockSpecTarget.parse(line.substring(0, selectorStartPos)); | |||
options = null; | |||
selector = line.substring(selectorStartPos+1); | |||
} | |||
} else { | |||
if (selectorStartPos == -1) { | |||
// no selector, split into target + options | |||
targets = BlockSpecTarget.parse(line.substring(0, optionStartPos)); | |||
options = StringUtil.splitAndTrim(line.substring(optionStartPos+1), ","); | |||
selector = null; | |||
} else { | |||
// all 3 elements present | |||
targets = BlockSpecTarget.parse(line.substring(0, optionStartPos)); | |||
options = StringUtil.splitAndTrim(line.substring(optionStartPos + 1, selectorStartPos), ","); | |||
selector = line.substring(selectorStartPos+1); | |||
} | |||
} | |||
final List<BlockSpec> specs = new ArrayList<>(); | |||
for (BlockSpecTarget target : targets) specs.add(new BlockSpec(line, target, options, selector)); | |||
return specs; | |||
} | |||
public boolean matches(String fqdn, String path) { | |||
if (target.hasDomainRegex() && target.getDomainPattern().matcher(fqdn).find()) { | |||
return checkDomainExclusionsAndType(fqdn, contentType(path)); | |||
} else if (target.hasRegex()) { | |||
if (target.getRegexPattern().matcher(path).find()) { | |||
return checkDomainExclusionsAndType(fqdn, contentType(path)); | |||
} | |||
final String full = fqdn + path; | |||
if (target.getRegexPattern().matcher(full).find()) { | |||
return checkDomainExclusionsAndType(fqdn, contentType(path)); | |||
}; | |||
} | |||
return false; | |||
} | |||
public boolean checkDomainExclusionsAndType(String fqdn, String contentType) { | |||
if (domainExclusions != null) { | |||
for (String domain : domainExclusions) { | |||
if (domain.equals(fqdn)) return false; | |||
} | |||
} | |||
if (typeExclusions != null) { | |||
for (String type : typeExclusions) { | |||
switch (type) { | |||
case OPT_SCRIPT: | |||
if (contentType.equals(HttpContentTypes.APPLICATION_JAVASCRIPT)) return false; | |||
break; | |||
case OPT_IMAGE: | |||
if (contentType.startsWith(HttpContentTypes.IMAGE_PREFIX)) return false; | |||
break; | |||
case OPT_STYLESHEET: | |||
if (contentType.equals(HttpContentTypes.TEXT_CSS)) return false; | |||
break; | |||
} | |||
} | |||
} | |||
if (typeMatches != null) { | |||
for (String type : typeMatches) { | |||
switch (type) { | |||
case OPT_SCRIPT: | |||
if (contentType.equals(HttpContentTypes.APPLICATION_JAVASCRIPT)) return true; | |||
break; | |||
case OPT_IMAGE: | |||
if (contentType.startsWith(HttpContentTypes.IMAGE_PREFIX)) return true; | |||
break; | |||
case OPT_STYLESHEET: | |||
if (contentType.equals(HttpContentTypes.TEXT_CSS)) return true; | |||
break; | |||
} | |||
} | |||
return false; | |||
} | |||
return true; | |||
} | |||
} |
@@ -0,0 +1,102 @@ | |||
package bubble.abp.spec; | |||
import lombok.EqualsAndHashCode; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.Setter; | |||
import lombok.experimental.Accessors; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import java.util.StringTokenizer; | |||
import java.util.regex.Pattern; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.http.HttpSchemes.stripScheme; | |||
@NoArgsConstructor @Accessors(chain=true) @EqualsAndHashCode(of={"domainRegex", "regex"}) | |||
public class BlockSpecTarget { | |||
@Getter @Setter private String domainRegex; | |||
public boolean hasDomainRegex() { return !empty(domainRegex); } | |||
@Getter(lazy=true) private final Pattern domainPattern = hasDomainRegex() ? Pattern.compile(getDomainRegex()) : null; | |||
@Getter @Setter private String regex; | |||
public boolean hasRegex() { return !empty(regex); } | |||
@Getter(lazy=true) private final Pattern regexPattern = hasRegex() ? Pattern.compile(getRegex()) : null; | |||
public static List<BlockSpecTarget> parse(String data) { | |||
final List<BlockSpecTarget> targets = new ArrayList<>(); | |||
for (String part : data.split(",")) { | |||
targets.add(parseTarget(part)); | |||
} | |||
return targets; | |||
} | |||
public static List<BlockSpecTarget> parseBareLine(String data) { | |||
if (data.contains("|") || data.contains("/") || data.contains("^")) return parse(data); | |||
final List<BlockSpecTarget> targets = new ArrayList<>(); | |||
for (String part : data.split(",")) { | |||
targets.add(new BlockSpecTarget().setDomainRegex(matchDomainOrAnySubdomains(part))); | |||
} | |||
return targets; | |||
} | |||
private static BlockSpecTarget parseTarget(String data) { | |||
String domainRegex = null; | |||
String regex = null; | |||
if (data.startsWith("||")) { | |||
final int caretPos = data.indexOf("^"); | |||
if (caretPos != -1) { | |||
// domain match | |||
final String domain = data.substring(2, caretPos); | |||
domainRegex = matchDomainOrAnySubdomains(domain); | |||
} else { | |||
final String domain = data.substring(2); | |||
domainRegex = matchDomainOrAnySubdomains(domain); | |||
} | |||
} else if (data.startsWith("|") && data.endsWith("|")) { | |||
// exact match | |||
final String verbatimMatch = stripScheme(data.substring(1, data.length() - 1)); | |||
regex = "^" + Pattern.quote(verbatimMatch) + "$"; | |||
} else if (data.startsWith("/")) { | |||
// path match, possibly regex | |||
if (data.endsWith("/") && ( | |||
data.contains("|") || data.contains("?") | |||
|| (data.contains("(") && data.contains(")")) | |||
|| (data.contains("{") && data.contains("}")))) { | |||
regex = data.substring(1, data.length()-1); | |||
} else if (data.contains("*")) { | |||
regex = parseWildcardMatch(data); | |||
} else { | |||
regex = "^" + Pattern.quote(data) + ".*"; | |||
} | |||
} else { | |||
if (data.contains("*")) { | |||
regex = parseWildcardMatch(data); | |||
} else { | |||
regex = "^" + Pattern.quote(data) + ".*"; | |||
} | |||
} | |||
return new BlockSpecTarget().setDomainRegex(domainRegex).setRegex(regex); | |||
} | |||
private static String parseWildcardMatch(String data) { | |||
final StringBuilder b = new StringBuilder("^"); | |||
final StringTokenizer st = new StringTokenizer(data, "*", true); | |||
while (st.hasMoreTokens()) { | |||
final String token = st.nextToken(); | |||
b.append(token.equals("*") ? ".*?" : token); | |||
} | |||
return b.append("$").toString(); | |||
} | |||
private static String matchDomainOrAnySubdomains(String domain) { | |||
return ".*?"+Pattern.quote(domain)+"$"; | |||
} | |||
} |
@@ -0,0 +1,48 @@ | |||
package bubble.abp.spec.selector; | |||
import lombok.*; | |||
import lombok.experimental.Accessors; | |||
import org.cobbzilla.util.collection.ArrayUtil; | |||
import static bubble.abp.spec.selector.SelectorParseError.parseError; | |||
@NoArgsConstructor @Accessors(chain=true) | |||
@EqualsAndHashCode @ToString | |||
public class AbpClause { | |||
@Getter @Setter private AbpClauseType type; | |||
@Getter @Setter private AbpContains contains; | |||
@Getter @Setter private AbpProperty[] properties; | |||
@Getter @Setter private BlockSelector selector; | |||
public static AbpClause buildAbpClause(AbpClauseType type, String spec) throws SelectorParseError { | |||
final AbpClause abp = new AbpClause().setType(type); | |||
if (!spec.startsWith("(") || !spec.endsWith(")")) throw parseError("expected abp clause to begin with open paren and end with close paren: "+spec); | |||
spec = spec.substring(1, spec.length()-1); | |||
switch (type) { | |||
case contains: | |||
return abp.setContains(AbpContains.build(spec)); | |||
case properties: | |||
if (spec.startsWith("/") && spec.endsWith("/")) { | |||
return abp.setProperties(new AbpProperty[]{ | |||
new AbpProperty() | |||
.setType(AbpPropertyType.regex) | |||
.setValue(spec.substring(1, spec.length()-1)) | |||
}); | |||
} | |||
AbpProperty[] props = new AbpProperty[0]; | |||
for (String prop : spec.split(";")) { | |||
props = ArrayUtil.append(props, AbpProperty.buildProperty(prop)); | |||
} | |||
return abp.setProperties(props); | |||
case has: | |||
if (!spec.startsWith(">")) throw parseError("expected abp-has argument to begin with > :"+spec); | |||
spec = spec.substring(1); | |||
return abp.setSelector(BlockSelector.buildNextSelector(spec.trim())); | |||
} | |||
throw parseError("invalid abp clause type: "+spec); | |||
} | |||
} |
@@ -0,0 +1,7 @@ | |||
package bubble.abp.spec.selector; | |||
public enum AbpClauseParseState { | |||
seeking_open_paren, seeking_close_paren; | |||
} |
@@ -0,0 +1,19 @@ | |||
package bubble.abp.spec.selector; | |||
import com.fasterxml.jackson.annotation.JsonCreator; | |||
import static bubble.abp.spec.selector.SelectorParseError.parseError; | |||
public enum AbpClauseType { | |||
properties, has, contains; | |||
public static final String ABP_CLAUSE_PREFIX = ":-abp-"; | |||
@JsonCreator public static AbpClauseType fromString (String v) { return valueOf(v.toLowerCase()); } | |||
public static AbpClauseType fromSpec(String spec) throws SelectorParseError { | |||
if (!spec.startsWith(ABP_CLAUSE_PREFIX)) throw parseError("invalid abp clause: "+spec); | |||
return valueOf(spec.substring(ABP_CLAUSE_PREFIX.length())); | |||
} | |||
} |
@@ -0,0 +1,27 @@ | |||
package bubble.abp.spec.selector; | |||
import lombok.*; | |||
import lombok.experimental.Accessors; | |||
@NoArgsConstructor @Accessors(chain=true) | |||
@EqualsAndHashCode @ToString | |||
public class AbpContains { | |||
@Getter @Setter private String value; | |||
@Getter @Setter private AbpContainsType type; | |||
@Getter @Setter private BlockSelector selector; | |||
public static AbpContains build(String spec) throws SelectorParseError { | |||
if (spec.startsWith("/") && (spec.endsWith("/") || spec.endsWith("/i"))) { | |||
return new AbpContains() | |||
.setValue(spec.substring(1, spec.length()-1)) | |||
.setType(spec.endsWith("/i") ? AbpContainsType.case_insensitive_regex : AbpContainsType.regex); | |||
} | |||
if (spec.contains("[") && spec.contains("]")) { | |||
return new AbpContains() | |||
.setSelector(BlockSelector.buildNextSelector(spec)) | |||
.setType(AbpContainsType.selector); | |||
} | |||
return new AbpContains().setType(AbpContainsType.literal).setValue(spec); | |||
} | |||
} |
@@ -0,0 +1,11 @@ | |||
package bubble.abp.spec.selector; | |||
import com.fasterxml.jackson.annotation.JsonCreator; | |||
public enum AbpContainsType { | |||
literal, regex, case_insensitive_regex, selector; | |||
@JsonCreator public static AbpContainsType fromString (String v) { return valueOf(v.toLowerCase()); } | |||
} |
@@ -0,0 +1,48 @@ | |||
package bubble.abp.spec.selector; | |||
import lombok.*; | |||
import lombok.experimental.Accessors; | |||
import java.util.StringTokenizer; | |||
import java.util.regex.Pattern; | |||
import static bubble.abp.spec.selector.SelectorParseError.parseError; | |||
@NoArgsConstructor @Accessors(chain=true) | |||
@EqualsAndHashCode @ToString | |||
public class AbpProperty { | |||
@Getter @Setter private String name; | |||
@Getter @Setter private AbpPropertyType type; | |||
@Getter @Setter private String value; | |||
public static AbpProperty buildProperty(String spec) throws SelectorParseError { | |||
final int colonPos = spec.indexOf(':'); | |||
if (colonPos == -1) throw parseError("invalid abp property (expecting colon): "+spec); | |||
if (colonPos == 0) throw parseError("invalid abp property (expecting name): "+spec); | |||
if (colonPos == spec.length()) throw parseError("invalid abp property (expecting value): "+spec); | |||
String value = spec.substring(colonPos + 1); | |||
if (value.contains("*")) { | |||
final StringBuilder regex = new StringBuilder(); | |||
final StringTokenizer st = new StringTokenizer(value, "*", true); | |||
while (st.hasMoreTokens()) { | |||
final String tok = st.nextToken(); | |||
if (tok.equals("*")) { | |||
regex.append(".+?"); | |||
} else { | |||
regex.append(Pattern.quote(tok)); | |||
} | |||
} | |||
return new AbpProperty() | |||
.setName(spec.substring(0, colonPos)) | |||
.setType(AbpPropertyType.wildcard) | |||
.setValue(regex.toString()); | |||
} else { | |||
return new AbpProperty() | |||
.setName(spec.substring(0, colonPos)) | |||
.setType(AbpPropertyType.exact) | |||
.setValue(value); | |||
} | |||
} | |||
} |
@@ -0,0 +1,11 @@ | |||
package bubble.abp.spec.selector; | |||
import com.fasterxml.jackson.annotation.JsonCreator; | |||
public enum AbpPropertyType { | |||
exact, wildcard, regex; | |||
@JsonCreator public static AbpPropertyType fromString (String v) { return valueOf(v.toLowerCase()); } | |||
} |
@@ -0,0 +1,165 @@ | |||
package bubble.abp.spec.selector; | |||
import lombok.*; | |||
import lombok.experimental.Accessors; | |||
import org.cobbzilla.util.collection.ArrayUtil; | |||
import java.util.StringTokenizer; | |||
import static bubble.abp.spec.selector.SelectorAttributeParseState.*; | |||
import static bubble.abp.spec.selector.SelectorParseError.parseError; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
@NoArgsConstructor @Accessors(chain=true) | |||
@ToString(of={"type", "abpEnabled", "name", "cls", "attributes", "abp", "operator", "next"}) | |||
@EqualsAndHashCode(of={"type", "abpEnabled", "name", "cls", "attributes", "abp", "operator", "next"}) | |||
public class BlockSelector { | |||
@Getter @Setter private String error; | |||
@Getter @Setter private SelectorType type; | |||
@Getter @Setter private Boolean abpEnabled; | |||
public boolean abpEnabled() { return abpEnabled != null && abpEnabled; } | |||
@Getter private String name; | |||
public BlockSelector setName(String n) throws SelectorParseError { | |||
final int dotPos = n.indexOf('.'); | |||
if (dotPos == -1) { | |||
name = n; | |||
} else { | |||
if (dotPos == 0 || dotPos == n.length()-1) throw parseError("invalid name: "+n); | |||
if (type == null) throw parseError("type not set, cannot set name: "+n); | |||
name = n.substring(0, dotPos); | |||
cls = n.substring(dotPos + 1); | |||
switch (type) { | |||
case id: type = SelectorType.id_and_cls; break; | |||
case tag: type = SelectorType.tag_and_cls; break; | |||
default: throw parseError("cannot add class to type "+type); | |||
} | |||
} | |||
return this; | |||
} | |||
@Getter @Setter private String cls; | |||
@Getter @Setter private SelectorAttribute[] attributes; | |||
@Getter @Setter private AbpClause abp; | |||
@Getter @Setter private SelectorOperator operator; | |||
@Getter @Setter private BlockSelector next; | |||
public static BlockSelector buildSelector(String spec) throws SelectorParseError { | |||
if (empty(spec)) return null; | |||
if (spec.charAt(0) != '#') throw parseError("expected spec to begin with '#'"); | |||
spec = spec.substring(1); | |||
final BlockSelector sel = new BlockSelector(); | |||
spec = SelectorType.setType(spec, sel); | |||
return _buildSelector(spec, sel); | |||
} | |||
public static BlockSelector buildNextSelector(String spec) throws SelectorParseError { | |||
if (empty(spec)) return null; | |||
final BlockSelector sel = new BlockSelector(); | |||
spec = SelectorType.setNextType(spec, sel); | |||
return _buildSelector(spec, sel); | |||
} | |||
private static BlockSelector _buildSelector(String spec, BlockSelector sel) throws SelectorParseError { | |||
boolean nameSet = false; | |||
int bracketPos = spec.indexOf('['); | |||
int spacePos = spec.indexOf(' '); | |||
int abpPos = spec.indexOf(":-"); | |||
if ((spacePos != -1 && bracketPos > spacePos) || (abpPos != -1 && bracketPos > abpPos)) { | |||
bracketPos = -1; | |||
} | |||
if (bracketPos != -1) { | |||
sel.setName(spec.substring(0, bracketPos)); | |||
spec = spec.substring(bracketPos); | |||
nameSet = true; | |||
final StringTokenizer st = new StringTokenizer(spec, "[] ", true); | |||
SelectorAttributeParseState state = seeking_open_bracket; | |||
String attrSpec = null; | |||
int charsConsumed = 0; | |||
while (st.hasMoreTokens()) { | |||
final String tok = st.nextToken(); | |||
charsConsumed += tok.length(); | |||
switch (tok) { | |||
case "[": | |||
if (state != seeking_open_bracket) throw parseError("invalid attribute (expecting open bracket): "+spec); | |||
state = seeking_close_bracket; | |||
break; | |||
case "]": | |||
if (state != seeking_close_bracket) throw parseError("invalid attribute (expecting close bracket): "+spec); | |||
state = seeking_open_bracket; | |||
sel.attributes = ArrayUtil.append(sel.attributes, SelectorAttribute.buildAttribute(attrSpec)); | |||
break; | |||
case " ": | |||
if (state != seeking_open_bracket) throw parseError("invalid attribute (unexpected space, expecting close bracket): "+spec); | |||
state = finished; | |||
break; | |||
default: | |||
attrSpec = tok; | |||
break; | |||
} | |||
if (state == finished) break; | |||
} | |||
spec = spec.substring(charsConsumed); | |||
} | |||
if (abpPos != -1 && spacePos != -1 && abpPos > spacePos) abpPos = -1; | |||
if (abpPos != -1) { | |||
sel.setName(spec.substring(0, abpPos)); | |||
nameSet = true; | |||
int openParen = spec.indexOf("("); | |||
if (openParen == -1) throw parseError("found abp clause but no open paren: "+spec); | |||
if (openParen == spec.length()-1) throw parseError("found abp clause but open paren has no closing paren: "+spec); | |||
final AbpClauseType abpClauseType = AbpClauseType.fromSpec(spec.substring(abpPos, openParen)); | |||
spec = spec.substring(openParen); | |||
int nestCount = 0; | |||
final StringTokenizer st = new StringTokenizer(spec, "()", true); | |||
boolean done = false; | |||
final StringBuilder abpSpec = new StringBuilder(); | |||
while (st.hasMoreTokens()) { | |||
final String tok = st.nextToken(); | |||
abpSpec.append(tok); | |||
switch (tok) { | |||
case "(": | |||
nestCount++; | |||
break; | |||
case ")": | |||
nestCount--; | |||
if (nestCount == 0) done = true; | |||
break; | |||
} | |||
if (done) break; | |||
} | |||
sel.setAbp(AbpClause.buildAbpClause(abpClauseType, abpSpec.toString())); | |||
spec = spec.substring(abpSpec.length()); | |||
} | |||
if (!nameSet) { | |||
spacePos = spec.indexOf(' '); | |||
if (spacePos == -1) { | |||
sel.setName(spec); | |||
return sel; | |||
} else { | |||
sel.setName(spec.substring(0, spacePos)); | |||
spec = spec.substring(spacePos).trim(); | |||
} | |||
} | |||
if (!empty(spec)) { | |||
spec = SelectorOperator.setOperator(spec, sel); | |||
sel.setNext(buildNextSelector(spec)); | |||
} | |||
return sel; | |||
} | |||
} |
@@ -0,0 +1,71 @@ | |||
package bubble.abp.spec.selector; | |||
import lombok.*; | |||
import lombok.experimental.Accessors; | |||
import org.cobbzilla.util.collection.ArrayUtil; | |||
import org.cobbzilla.util.collection.NameAndValue; | |||
import static bubble.abp.spec.selector.SelectorParseError.parseError; | |||
@NoArgsConstructor @Accessors(chain=true) | |||
@ToString(of={"name", "comparison", "value", "style"}) | |||
@EqualsAndHashCode(of={"name", "comparison", "value", "style"}) | |||
public class SelectorAttribute { | |||
@Getter @Setter private String name; | |||
@Getter @Setter private SelectorAttributeComparison comparison; | |||
@Getter @Setter private String value; | |||
@Getter @Setter private NameAndValue[] style; | |||
public static SelectorAttribute buildAttribute(String spec) throws SelectorParseError { | |||
final int eqPos = spec.indexOf('='); | |||
if (eqPos == -1) throw parseError("invalid attribute ('=' not found): "+spec); | |||
if (eqPos == 0) throw parseError("invalid attribute (nothing precedes '='): "+spec); | |||
final SelectorAttribute attr = new SelectorAttribute(); | |||
switch (spec.charAt(eqPos-1)) { | |||
case '^': | |||
attr.setComparison(SelectorAttributeComparison.startsWith); | |||
attr.setName(spec.substring(0, eqPos-1)); | |||
break; | |||
case '$': | |||
attr.setComparison(SelectorAttributeComparison.endsWith); | |||
attr.setName(spec.substring(0, eqPos-1)); | |||
break; | |||
case '*': | |||
attr.setComparison(SelectorAttributeComparison.contains); | |||
attr.setName(spec.substring(0, eqPos-1)); | |||
break; | |||
default: | |||
attr.setComparison(SelectorAttributeComparison.equals); | |||
attr.setName(spec.substring(0, eqPos)); | |||
break; | |||
} | |||
final int openQuote = spec.indexOf("\""); | |||
if (openQuote == -1) throw parseError("invalid attribute (expected opening double-quote char following '='): "+spec); | |||
final int closeQuote = spec.indexOf("\"", openQuote+1); | |||
if (closeQuote == -1) throw parseError("invalid attribute (expected closing double-quote char after opening double-quote char): "+spec); | |||
attr.setValue(spec.substring(openQuote + 1, closeQuote)); | |||
if (attr.getName().equalsIgnoreCase("style")) { | |||
attr.setStyle(buildStyles(spec.substring(openQuote + 1, closeQuote))); | |||
} | |||
return attr; | |||
} | |||
private static NameAndValue[] buildStyles(String spec) throws SelectorParseError { | |||
NameAndValue[] styles = new NameAndValue[0]; | |||
for (String part : spec.split(";")) { | |||
styles = ArrayUtil.append(styles, buildStyle(part)); | |||
} | |||
return styles; | |||
} | |||
private static NameAndValue buildStyle(String style) throws SelectorParseError { | |||
final int colonPos = style.indexOf(':'); | |||
if (colonPos == -1) throw parseError("invalid style (expected colon char): "+style); | |||
if (colonPos == 0 || colonPos == style.length()-1) throw parseError("invalid style (no name or value): "+style); | |||
return new NameAndValue(style.substring(0, colonPos), style.substring(colonPos+1)); | |||
} | |||
} |
@@ -0,0 +1,11 @@ | |||
package bubble.abp.spec.selector; | |||
import com.fasterxml.jackson.annotation.JsonCreator; | |||
public enum SelectorAttributeComparison { | |||
equals, startsWith, endsWith, contains; | |||
@JsonCreator public static SelectorAttributeComparison fromString (String v) { return valueOf(v.toLowerCase()); } | |||
} |
@@ -0,0 +1,7 @@ | |||
package bubble.abp.spec.selector; | |||
public enum SelectorAttributeParseState { | |||
seeking_open_bracket, seeking_close_bracket, finished; | |||
} |
@@ -0,0 +1,35 @@ | |||
package bubble.abp.spec.selector; | |||
import com.fasterxml.jackson.annotation.JsonCreator; | |||
import static bubble.abp.spec.selector.SelectorParseError.parseError; | |||
public enum SelectorOperator { | |||
next, encloses; | |||
@JsonCreator public static SelectorOperator fromString (String v) { return valueOf(v.toLowerCase()); } | |||
public static String setOperator(String spec, BlockSelector sel) throws SelectorParseError { | |||
spec = spec.trim(); | |||
if (spec.length() == 0) throw parseError("unexpected end of spec"); | |||
switch (spec.charAt(0)) { | |||
case '+': | |||
sel.setOperator(next); | |||
if (spec.charAt(1) != ' ') throw parseError("expected space after + operator: "+spec); | |||
spec = spec.substring(2); | |||
break; | |||
case '>': | |||
sel.setOperator(encloses); | |||
if (spec.charAt(1) != ' ') throw parseError("expected space after > operator: "+spec); | |||
spec = spec.substring(2); | |||
break; | |||
default: | |||
// non-standard syntax, roll with it, assume they mean to enclose | |||
sel.setOperator(encloses); | |||
if (!Character.isAlphabetic(spec.charAt(0))) throw parseError("expected alphabetic char: "+spec); | |||
break; | |||
} | |||
return spec; | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
package bubble.abp.spec.selector; | |||
public class SelectorParseError extends RuntimeException { | |||
public SelectorParseError(String s) { super(s); } | |||
public static SelectorParseError parseError(String s) { return new SelectorParseError(s); } | |||
} |
@@ -0,0 +1,36 @@ | |||
package bubble.abp.spec.selector; | |||
import com.fasterxml.jackson.annotation.JsonCreator; | |||
import static bubble.abp.spec.selector.SelectorParseError.parseError; | |||
public enum SelectorType { | |||
id, tag, cls, id_and_cls, tag_and_cls; | |||
@JsonCreator public static SelectorType fromString (String v) { return valueOf(v.toLowerCase()); } | |||
public static String setType(String spec, BlockSelector sel) throws SelectorParseError { | |||
if (spec.startsWith("?")) { | |||
sel.setAbpEnabled(true); | |||
spec = spec.substring(1); | |||
} | |||
if (!spec.startsWith("#")) throw parseError("invalid prefix: "+spec); | |||
return setNextType(spec.substring(1), sel); | |||
} | |||
public static String setNextType(String spec, BlockSelector sel) throws SelectorParseError { | |||
if (spec.startsWith("#")) { | |||
sel.setType(id); | |||
spec = spec.substring(1); | |||
} else if (spec.startsWith(".")) { | |||
sel.setType(cls); | |||
spec = spec.substring(1); | |||
} else { | |||
sel.setType(tag); | |||
} | |||
return spec; | |||
} | |||
} |
@@ -0,0 +1,157 @@ | |||
package bubble.abp.spec; | |||
import org.junit.Test; | |||
import java.util.Arrays; | |||
import static org.junit.Assert.assertEquals; | |||
public class BlockListTest { | |||
public static final String BLOCK = BlockDecisionType.block.name(); | |||
public static final String ALLOW = BlockDecisionType.allow.name(); | |||
public static final String FILTER = BlockDecisionType.filter.name(); | |||
public static final String[][] BLOCK_TESTS = { | |||
// rule // fqdn // path // expected decision | |||
// bare hosts example (ala EasyList) | |||
{"example.com", "example.com", "/some_path", BLOCK}, | |||
{"example.com", "foo.example.com", "/some_path", BLOCK}, | |||
{"example.com", "example.org", "/some_path", ALLOW}, | |||
// block example.com and all subdomains | |||
{"||example.com^", "example.com", "/some_path", BLOCK}, | |||
{"||example.com^", "foo.example.com", "/some_path", BLOCK}, | |||
{"||example.com^", "example.org", "/some_path", ALLOW}, | |||
// block exact string | |||
{"|example.com/|", "example.com", "/", BLOCK}, | |||
{"|example.com/|", "example.com", "/some_path", ALLOW}, | |||
{"|example.com/|", "foo.example.com", "/some_path", ALLOW}, | |||
// block example.com, but not foo.example.com or bar.example.com | |||
{"||example.com^$domain=~foo.example.com|~bar.example.com", | |||
"example.com", "/some_path", BLOCK}, | |||
{"||example.com^$domain=~foo.example.com|~bar.example.com", | |||
"foo.example.com", "/some_path", ALLOW}, | |||
{"||example.com^$domain=~foo.example.com|~bar.example.com", | |||
"bar.example.com", "/some_path", ALLOW}, | |||
{"||example.com^$domain=~foo.example.com|~bar.example.com", | |||
"baz.example.com", "/some_path", BLOCK}, | |||
// block images and scripts on example.com, but not foo.example.com or bar.example.com | |||
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com", | |||
"example.com", "/some_path", ALLOW}, | |||
// test image blocking | |||
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com", | |||
"example.com", "/some_path.png", BLOCK}, | |||
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com", | |||
"foo.example.com", "/some_path.png", ALLOW}, | |||
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com", | |||
"bar.example.com", "/some_path.png", ALLOW}, | |||
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com", | |||
"baz.example.com", "/some_path.png", BLOCK}, | |||
// test script blocking | |||
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com", | |||
"example.com", "/some_path.js", BLOCK}, | |||
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com", | |||
"foo.example.com", "/some_path.js", ALLOW}, | |||
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com", | |||
"bar.example.com", "/some_path.js", ALLOW}, | |||
{"||example.com^$image,script,domain=~foo.example.com|~bar.example.com", | |||
"baz.example.com", "/some_path.js", BLOCK}, | |||
// test stylesheet blocking | |||
{"||example.com^stylesheet,domain=~foo.example.com|~bar.example.com", | |||
"example.com", "/some_path.css", BLOCK}, | |||
{"||example.com^$stylesheet,domain=~foo.example.com|~bar.example.com", | |||
"foo.example.com", "/some_path.css", ALLOW}, | |||
{"||example.com^$stylesheet,domain=~foo.example.com|~bar.example.com", | |||
"bar.example.com", "/some_path.css", ALLOW}, | |||
{"||example.com^$stylesheet,domain=~foo.example.com|~bar.example.com", | |||
"baz.example.com", "/some_path.css", BLOCK}, | |||
// path matching | |||
{"/foo", "example.com", "/some_path", ALLOW}, | |||
{"/foo", "example.com", "/foo", BLOCK}, | |||
{"/foo", "example.com", "/foo/bar", BLOCK}, | |||
// path matching with wildcard | |||
{"/foo/*/img", "example.com", "/some_path", ALLOW}, | |||
{"/foo/*/img", "example.com", "/foo", ALLOW}, | |||
{"/foo/*/img", "example.com", "/foo/img", ALLOW}, | |||
{"/foo/*/img", "example.com", "/foo/x/img", BLOCK}, | |||
{"/foo/*/img", "example.com", "/foo/x/img.png", ALLOW}, | |||
{"/foo/*/img", "example.com", "/foo/x/y/z//img", BLOCK}, | |||
// path matching with regex | |||
{"/foo/(apps|ads)/img.+/", "example.com", "/foo/x/y/z//img", ALLOW}, | |||
{"/foo/(apps|ads)/img.+/", "example.com", "/foo/apps/img.png", BLOCK}, | |||
{"/foo/(apps|ads)/img.+/", "example.com", "/foo/ads/img.png", BLOCK}, | |||
{"/foo/(apps|ads)/img.+/", "example.com", "/foo/bar/ads/img.png", ALLOW}, | |||
{"/(apps|ads)\\.example\\.(com|org)/", | |||
"example.com", "/ad.png", ALLOW}, | |||
{"/(apps|ads)\\.example\\.(com|org)/", | |||
"ads.example.com", "/ad.png", BLOCK}, | |||
{"/(apps|ads)\\.example\\.(com|org)/", | |||
"apps.example.com", "/ad.png", BLOCK}, | |||
{"/(apps|ads)\\.example\\.(com|org)/", | |||
"ads.example.org", "/ad.png", BLOCK}, | |||
{"/(apps|ads)\\.example\\.(com|org)/", | |||
"apps.example.org", "/ad.png", BLOCK}, | |||
{"/(apps|ads)\\.example\\.(com|org)/", | |||
"ads.example.net", "/ad.png", ALLOW}, | |||
{"/(apps|ads)\\.example\\.(com|org)/", | |||
"apps.example.net", "/ad.png", ALLOW}, | |||
// selectors | |||
{"example.com##.banner-ad", "example.com", "/ad.png", FILTER}, | |||
// putting it all together | |||
{"||example.com^$domain=~foo.example.com|~bar.example.com##.banner-ad", | |||
"example.com", "/some_path", FILTER}, | |||
{"||example.com^$domain=~foo.example.com|~bar.example.com##.banner-ad", | |||
"baz.example.com", "/some_path", FILTER}, | |||
{"||example.com^$domain=~foo.example.com|~bar.example.com##.banner-ad", | |||
"foo.example.com", "/some_path", FILTER}, | |||
{"||example.com^$domain=~foo.example.com|~bar.example.com##.banner-ad", | |||
"bar.example.com", "/some_path", FILTER}, | |||
}; | |||
@Test public void testRules () throws Exception { | |||
for (String[] test : BLOCK_TESTS) { | |||
final BlockDecisionType expectedDecision = BlockDecisionType.fromString(test[3]); | |||
final BlockList blockList = new BlockList(); | |||
blockList.addToBlacklist(BlockSpec.parse(test[0])); | |||
assertEquals("testBlanketBlock: expected "+expectedDecision+" decision, test=" + Arrays.toString(test), | |||
expectedDecision, | |||
blockList.getDecision(test[1], test[2]).getDecisionType()); | |||
} | |||
} | |||
public String[] SELECTOR_SPECS = { | |||
"||example.com##.banner-ad", | |||
"||foo.example.com##.more-ads", | |||
}; | |||
@Test public void testMultipleSelectorMatches () throws Exception { | |||
final BlockList blockList = new BlockList(); | |||
for (String line : SELECTOR_SPECS) { | |||
blockList.addToBlacklist(BlockSpec.parse(line)); | |||
} | |||
BlockDecision decision; | |||
decision = blockList.getDecision("example.com", "/some_path"); | |||
assertEquals("expected filter decision", BlockDecisionType.filter, decision.getDecisionType()); | |||
assertEquals("expected 1 filter specs", 2, decision.getSpecs().size()); | |||
decision = blockList.getDecision("foo.example.com", "/some_path"); | |||
assertEquals("expected filter decision", BlockDecisionType.filter, decision.getDecisionType()); | |||
assertEquals("expected 2 filter specs", 2, decision.getSpecs().size()); | |||
} | |||
} |
@@ -0,0 +1,135 @@ | |||
package bubble.abp.spec; | |||
import bubble.abp.spec.selector.*; | |||
import org.cobbzilla.util.collection.NameAndValue; | |||
import org.junit.Test; | |||
import static bubble.abp.spec.selector.SelectorAttributeComparison.*; | |||
import static bubble.abp.spec.selector.SelectorOperator.encloses; | |||
import static bubble.abp.spec.selector.SelectorOperator.next; | |||
import static bubble.abp.spec.selector.SelectorType.*; | |||
import static org.junit.Assert.assertEquals; | |||
public class SelectorTest { | |||
public static final Object[][] SELECTOR_TESTS = new Object[][] { | |||
{"###foo", new BlockSelector().setType(id).setName("foo")}, | |||
{"##.foo", new BlockSelector().setType(cls).setName("foo")}, | |||
{"##foo", new BlockSelector().setType(tag).setName("foo")}, | |||
{"##foo > .bar", new BlockSelector().setType(tag).setName("foo").setOperator(encloses) | |||
.setNext(new BlockSelector().setType(cls).setName("bar"))}, | |||
{"##foo + .bar", new BlockSelector().setType(tag).setName("foo").setOperator(next) | |||
.setNext(new BlockSelector().setType(cls).setName("bar"))}, | |||
{"##foo > bar", new BlockSelector().setType(tag).setName("foo").setOperator(encloses) | |||
.setNext(new BlockSelector().setType(tag).setName("bar"))}, | |||
{"##foo + bar", new BlockSelector().setType(tag).setName("foo").setOperator(next) | |||
.setNext(new BlockSelector().setType(tag).setName("bar"))}, | |||
{"##.foo[href=\"bar\"]", new BlockSelector().setType(cls).setName("foo").setAttributes(new SelectorAttribute[] { | |||
new SelectorAttribute().setName("href").setComparison(equals).setValue("bar") | |||
})}, | |||
{"##.foo[href^=\"bar\"]", new BlockSelector().setType(cls).setName("foo").setAttributes(new SelectorAttribute[] { | |||
new SelectorAttribute().setName("href").setComparison(startsWith).setValue("bar") | |||
})}, | |||
{"##.foo[href$=\"bar\"]", new BlockSelector().setType(cls).setName("foo").setAttributes(new SelectorAttribute[] { | |||
new SelectorAttribute().setName("href").setComparison(endsWith).setValue("bar") | |||
})}, | |||
{"##.foo[href*=\"bar\"]", new BlockSelector().setType(cls).setName("foo").setAttributes(new SelectorAttribute[] { | |||
new SelectorAttribute().setName("href").setComparison(contains).setValue("bar") | |||
})}, | |||
{"##.foo[href=\"bar\"][target^=\"_blank\"]", new BlockSelector().setType(cls).setName("foo").setAttributes(new SelectorAttribute[] { | |||
new SelectorAttribute().setName("href").setComparison(equals).setValue("bar"), | |||
new SelectorAttribute().setName("target").setComparison(startsWith).setValue("_blank") | |||
})}, | |||
{"##.foo[href=\"bar\"][target^=\"_blank\"][style*=\"width:300px;height:100px\"]", new BlockSelector().setType(cls).setName("foo").setAttributes(new SelectorAttribute[] { | |||
new SelectorAttribute().setName("href").setComparison(equals).setValue("bar"), | |||
new SelectorAttribute().setName("target").setComparison(startsWith).setValue("_blank"), | |||
new SelectorAttribute().setName("style").setComparison(contains).setValue("width:300px;height:100px") | |||
.setStyle(new NameAndValue[] { | |||
new NameAndValue("width", "300px"), | |||
new NameAndValue("height", "100px") | |||
}) | |||
})}, | |||
{"##foo > .bar[src*=\"advert\"][height=\"30\"] + img > #ad_foo[src$=\".png\"]", new BlockSelector().setType(tag).setName("foo") | |||
.setOperator(encloses) | |||
.setNext(new BlockSelector().setType(cls).setName("bar").setAttributes( | |||
new SelectorAttribute[] { | |||
new SelectorAttribute().setName("src").setComparison(contains).setValue("advert"), | |||
new SelectorAttribute().setName("height").setComparison(equals).setValue("30") | |||
}) | |||
.setOperator(next) | |||
.setNext(new BlockSelector().setType(tag).setName("img") | |||
.setOperator(encloses) | |||
.setNext(new BlockSelector().setType(id).setName("ad_foo").setAttributes( | |||
new SelectorAttribute[] { | |||
new SelectorAttribute().setName("src").setComparison(endsWith).setValue(".png") | |||
})) | |||
)) | |||
}, | |||
}; | |||
public static final Object[][] ABP_TESTS = new Object[][] { | |||
{"#?#div:-abp-properties(width:300px;height:250px;)", new BlockSelector() | |||
.setAbpEnabled(true).setType(tag).setName("div").setAbp( | |||
new AbpClause() | |||
.setType(AbpClauseType.properties) | |||
.setProperties(new AbpProperty[] { | |||
new AbpProperty().setName("width").setType(AbpPropertyType.exact).setValue("300px"), | |||
new AbpProperty().setName("height").setType(AbpPropertyType.exact).setValue("250px") | |||
})) | |||
}, | |||
{"#?#div:-abp-has(> div > img.advert)", new BlockSelector() | |||
.setAbpEnabled(true).setType(tag).setName("div").setAbp( | |||
new AbpClause() | |||
.setType(AbpClauseType.has) | |||
.setSelector(new BlockSelector().setType(tag).setName("div").setOperator(encloses) | |||
.setNext(new BlockSelector().setType(tag_and_cls).setName("img").setCls("advert"))))}, | |||
{"#?#div:-abp-has(> span:-abp-contains(Advertisement))", new BlockSelector() | |||
.setAbpEnabled(true).setType(tag).setName("div").setAbp( | |||
new AbpClause().setType(AbpClauseType.has) | |||
.setSelector(new BlockSelector().setType(tag).setName("span").setAbp( | |||
new AbpClause().setType(AbpClauseType.contains).setContains( | |||
new AbpContains().setType(AbpContainsType.literal).setValue("Advertisement") | |||
))))}, | |||
{"#?#div > img:-abp-properties(/width: 3[2-8]px;/)", new BlockSelector() | |||
.setAbpEnabled(true).setType(tag).setName("div").setOperator(encloses).setNext( | |||
new BlockSelector().setType(tag).setName("img").setAbp( | |||
new AbpClause().setType(AbpClauseType.properties).setProperties(new AbpProperty[]{ | |||
new AbpProperty().setType(AbpPropertyType.regex).setValue("width: 3[2-8]px;") | |||
})))}, | |||
{"#?#.panel:-abp-contains(a[href*=\"example.com\"])", new BlockSelector() | |||
.setAbpEnabled(true).setType(cls).setName("panel").setAbp( | |||
new AbpClause().setType(AbpClauseType.contains).setContains( | |||
new AbpContains().setType(AbpContainsType.selector).setSelector( | |||
new BlockSelector().setType(tag).setName("a").setAttributes(new SelectorAttribute[]{ | |||
new SelectorAttribute().setName("href").setComparison(contains).setValue("example.com") | |||
}))))}, | |||
{"#?#.cls-content ol:-abp-contains(Download Foo)", new BlockSelector() | |||
.setAbpEnabled(true).setType(cls).setName("cls-content").setOperator(encloses).setNext( | |||
new BlockSelector().setType(tag).setName("ol").setAbp( | |||
new AbpClause().setType(AbpClauseType.contains).setContains( | |||
new AbpContains().setType(AbpContainsType.literal).setValue("Download Foo") | |||
)))}}; | |||
@Test public void testSelectorParsing () throws Exception { runTests(SELECTOR_TESTS); } | |||
@Test public void testAbpParsing () throws Exception { runTests(ABP_TESTS); } | |||
private void runTests(Object[][] tests) throws SelectorParseError { | |||
for (Object[] test : tests) { | |||
final String spec = test[0].toString(); | |||
final BlockSelector sel = BlockSelector.buildSelector(spec); | |||
assertEquals("BlockSelector object is incorrect for spec: "+spec, test[1], sel); | |||
} | |||
} | |||
} |