@@ -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); | |||||
} | |||||
} | |||||
} |