275 lines
8.8 KiB
Java
275 lines
8.8 KiB
Java
package org.julianfamily.keepassgo;
|
|
|
|
import java.net.URI;
|
|
import java.util.ArrayList;
|
|
import java.util.Comparator;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
|
|
final class AutofillTargetMatcher {
|
|
private AutofillTargetMatcher() {
|
|
}
|
|
|
|
static Entry findBestMatch(List<Entry> entries, String rawTarget) {
|
|
if (entries == null || entries.isEmpty()) {
|
|
return null;
|
|
}
|
|
NormalizedTarget target = normalize(rawTarget);
|
|
if (target.host.isEmpty()) {
|
|
return null;
|
|
}
|
|
List<Entry> exactHost = new ArrayList<>();
|
|
List<Entry> parentHost = new ArrayList<>();
|
|
for (Entry entry : entries) {
|
|
if (entryMatchesHost(entry, target.host)) {
|
|
exactHost.add(entry);
|
|
continue;
|
|
}
|
|
if (entryMatchesParentHost(entry, target.host)) {
|
|
parentHost.add(entry);
|
|
}
|
|
}
|
|
Entry matched = chooseEntry(target, exactHost);
|
|
if (matched != null) {
|
|
return matched;
|
|
}
|
|
return chooseEntry(target, parentHost);
|
|
}
|
|
|
|
static List<Entry> chooserCandidates(List<Entry> entries, String rawTarget) {
|
|
if (entries == null || entries.isEmpty()) {
|
|
return new ArrayList<>();
|
|
}
|
|
List<Entry> related = relevantCandidates(entries, rawTarget);
|
|
if (!related.isEmpty()) {
|
|
return related;
|
|
}
|
|
return sortEntries(entries);
|
|
}
|
|
|
|
static List<Entry> relevantCandidates(List<Entry> entries, String rawTarget) {
|
|
if (entries == null || entries.isEmpty()) {
|
|
return new ArrayList<>();
|
|
}
|
|
Entry direct = findBestMatch(entries, rawTarget);
|
|
if (direct != null) {
|
|
List<Entry> resolved = new ArrayList<>();
|
|
resolved.add(direct);
|
|
return resolved;
|
|
}
|
|
NormalizedTarget target = normalize(rawTarget);
|
|
if (target.host.isEmpty()) {
|
|
return new ArrayList<>();
|
|
}
|
|
List<Entry> exactHost = new ArrayList<>();
|
|
List<Entry> parentHost = new ArrayList<>();
|
|
for (Entry entry : entries) {
|
|
if (entryMatchesHost(entry, target.host)) {
|
|
exactHost.add(entry);
|
|
continue;
|
|
}
|
|
if (entryMatchesParentHost(entry, target.host)) {
|
|
parentHost.add(entry);
|
|
}
|
|
}
|
|
if (!exactHost.isEmpty()) {
|
|
return sortEntries(exactHost);
|
|
}
|
|
if (!parentHost.isEmpty()) {
|
|
return sortEntries(parentHost);
|
|
}
|
|
return new ArrayList<>();
|
|
}
|
|
|
|
static NormalizedTarget normalize(String raw) {
|
|
if (raw == null) {
|
|
return new NormalizedTarget("", "", "");
|
|
}
|
|
String trimmed = raw.trim();
|
|
if (trimmed.isEmpty()) {
|
|
return new NormalizedTarget("", "", "");
|
|
}
|
|
String original = trimmed;
|
|
String value = trimmed.toLowerCase(Locale.US);
|
|
if (value.startsWith("http://")) {
|
|
value = value.substring("http://".length());
|
|
} else if (value.startsWith("https://")) {
|
|
value = value.substring("https://".length());
|
|
}
|
|
int slash = value.indexOf('/');
|
|
if (slash >= 0) {
|
|
value = value.substring(0, slash);
|
|
}
|
|
int colon = value.indexOf(':');
|
|
if (colon >= 0) {
|
|
value = value.substring(0, colon);
|
|
}
|
|
String host = value;
|
|
String path = "/";
|
|
int schemeSep = original.indexOf("://");
|
|
if (schemeSep < 0) {
|
|
original = "https://" + original;
|
|
}
|
|
try {
|
|
URI uri = URI.create(original);
|
|
if (uri.getHost() != null) {
|
|
host = uri.getHost().toLowerCase(Locale.US);
|
|
}
|
|
path = cleanPath(uri.getPath());
|
|
} catch (IllegalArgumentException ignored) {
|
|
path = "/";
|
|
}
|
|
return new NormalizedTarget(host, path, host + path);
|
|
}
|
|
|
|
private static String cleanPath(String raw) {
|
|
if (raw == null || raw.trim().isEmpty() || "/".equals(raw.trim())) {
|
|
return "/";
|
|
}
|
|
String value = raw.trim();
|
|
while (value.endsWith("/") && value.length() > 1) {
|
|
value = value.substring(0, value.length() - 1);
|
|
}
|
|
if (!value.startsWith("/")) {
|
|
value = "/" + value;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private static Entry chooseEntry(NormalizedTarget target, List<Entry> entries) {
|
|
if (entries.isEmpty()) {
|
|
return null;
|
|
}
|
|
if (entries.size() == 1) {
|
|
return entries.get(0);
|
|
}
|
|
List<Entry> exact = new ArrayList<>();
|
|
List<Entry> prefix = new ArrayList<>();
|
|
int bestPrefixLen = -1;
|
|
for (Entry entry : entries) {
|
|
MatchQuality quality = bestTargetMatch(entry, target);
|
|
if (quality.exact) {
|
|
exact.add(entry);
|
|
continue;
|
|
}
|
|
if (quality.prefixLength <= 0) {
|
|
continue;
|
|
}
|
|
if (quality.prefixLength > bestPrefixLen) {
|
|
prefix.clear();
|
|
prefix.add(entry);
|
|
bestPrefixLen = quality.prefixLength;
|
|
} else if (quality.prefixLength == bestPrefixLen) {
|
|
prefix.add(entry);
|
|
}
|
|
}
|
|
if (exact.size() == 1) {
|
|
return exact.get(0);
|
|
}
|
|
if (exact.size() > 1 || prefix.isEmpty()) {
|
|
return null;
|
|
}
|
|
return prefix.size() == 1 ? prefix.get(0) : null;
|
|
}
|
|
|
|
private static boolean entryMatchesHost(Entry entry, String host) {
|
|
for (NormalizedTarget target : entryTargets(entry)) {
|
|
if (target.host.equals(host)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static boolean entryMatchesParentHost(Entry entry, String host) {
|
|
for (NormalizedTarget target : entryTargets(entry)) {
|
|
if (!target.host.isEmpty() && host.endsWith("." + target.host)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static List<NormalizedTarget> entryTargets(Entry entry) {
|
|
List<String> rawTargets = entry.targets;
|
|
if (rawTargets.isEmpty()) {
|
|
rawTargets = new ArrayList<>();
|
|
rawTargets.add(entry.url);
|
|
}
|
|
List<NormalizedTarget> targets = new ArrayList<>();
|
|
for (String rawTarget : rawTargets) {
|
|
NormalizedTarget target = normalize(rawTarget);
|
|
if (!target.host.isEmpty()) {
|
|
targets.add(target);
|
|
}
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
private static MatchQuality bestTargetMatch(Entry entry, NormalizedTarget target) {
|
|
int bestPrefixLen = -1;
|
|
for (NormalizedTarget entryTarget : entryTargets(entry)) {
|
|
if (entryTarget.url.equals(target.url)) {
|
|
return new MatchQuality(true, 0);
|
|
}
|
|
if (!"/".equals(entryTarget.path) && target.path.startsWith(entryTarget.path)) {
|
|
bestPrefixLen = Math.max(bestPrefixLen, entryTarget.path.length());
|
|
}
|
|
}
|
|
return new MatchQuality(false, bestPrefixLen);
|
|
}
|
|
|
|
private static List<Entry> sortEntries(List<Entry> entries) {
|
|
List<Entry> sorted = new ArrayList<>(entries);
|
|
sorted.sort(Comparator
|
|
.comparing((Entry entry) -> entry.title.toLowerCase(Locale.US))
|
|
.thenComparing(entry -> String.join("/", entry.path).toLowerCase(Locale.US))
|
|
.thenComparing(entry -> entry.id));
|
|
return sorted;
|
|
}
|
|
|
|
static final class NormalizedTarget {
|
|
final String host;
|
|
final String path;
|
|
final String url;
|
|
|
|
NormalizedTarget(String host, String path, String url) {
|
|
this.host = host;
|
|
this.path = path;
|
|
this.url = url;
|
|
}
|
|
}
|
|
|
|
static final class Entry {
|
|
final String id;
|
|
final String title;
|
|
final String username;
|
|
final String password;
|
|
final String host;
|
|
final String url;
|
|
final List<String> targets;
|
|
final List<String> path;
|
|
|
|
Entry(String id, String title, String username, String password, String host, String url, List<String> targets, List<String> path) {
|
|
this.id = id;
|
|
this.title = title;
|
|
this.username = username;
|
|
this.password = password;
|
|
this.host = host;
|
|
this.url = url;
|
|
this.targets = new ArrayList<>(targets);
|
|
this.path = new ArrayList<>(path);
|
|
}
|
|
}
|
|
|
|
private static final class MatchQuality {
|
|
final boolean exact;
|
|
final int prefixLength;
|
|
|
|
MatchQuality(boolean exact, int prefixLength) {
|
|
this.exact = exact;
|
|
this.prefixLength = prefixLength;
|
|
}
|
|
}
|
|
}
|