Improve autofill matching (#801)

This commit is contained in:
Leendert de Borst
2025-04-15 16:36:45 +02:00
committed by Leendert de Borst
parent 22d2e09982
commit 1d77d05e7c
2 changed files with 74 additions and 33 deletions

View File

@@ -1,63 +1,103 @@
import { CombinedStopWords } from "@/utils/formDetector/FieldPatterns";
import { Credential } from "../../utils/types/Credential";
interface CredentialWithPriority extends Credential {
priority: number;
}
/**
* Filter credentials based on current URL and page context to determine which credentials to show
* in the autofill popup.
* in the autofill popup. Credentials are sorted by priority:
* 1. Exact URL match (highest priority)
* 2. Base URL match AND page title word match
* 3. Base URL match only
* 4. Page title word match only (lowest priority)
*/
export function filterCredentials(credentials: Credential[], currentUrl: string, pageTitle: string): Credential[] {
const urlObject = new URL(currentUrl);
const baseUrl = `${urlObject.protocol}//${urlObject.hostname}`;
const filtered: CredentialWithPriority[] = [];
// 1. Exact URL match
let filtered = credentials.filter(cred =>
cred.ServiceUrl?.toLowerCase() === currentUrl.toLowerCase()
);
const sanitizedCurrentUrl = currentUrl.toLowerCase().replace('www.', '');
// 2. Base URL match with fuzzy domain comparison if no exact matches
filtered = filtered.concat(credentials.filter(cred => {
if (!cred.ServiceUrl) {
return false;
// 1. Exact URL match (priority 1)
credentials.forEach(cred => {
if (!cred.ServiceUrl || cred.ServiceUrl.length === 0) {
return;
}
const sanitizedCredUrl = cred.ServiceUrl.toLowerCase().replace('www.', '');
if (sanitizedCurrentUrl.startsWith(sanitizedCredUrl)) {
filtered.push({ ...cred, priority: 1 });
}
});
// If we have one or more exact matches, do not continue to other matches
if (filtered.length > 0) {
return filtered;
}
// Prepare page title words for matching
const titleWords = pageTitle.length > 0
? pageTitle.toLowerCase()
.split(/\s+/)
.filter(word =>
word.length > 3 &&
!CombinedStopWords.has(word.toLowerCase())
)
: [];
// Check for base URL matches and page title matches
credentials.forEach(cred => {
if (!cred.ServiceUrl || filtered.some(f => f.Id === cred.Id)) {
return;
}
let hasBaseUrlMatch = false;
let hasTitleMatch = false;
// Check base URL match
try {
const credUrlObject = new URL(cred.ServiceUrl);
const currentUrlObject = new URL(baseUrl);
// Extract root domains by splitting on dots and taking last two parts
const credDomainParts = credUrlObject.hostname.toLowerCase().split('.');
const currentDomainParts = currentUrlObject.hostname.toLowerCase().split('.');
// Get root domain (last two parts, e.g., 'aliasvault.net')
const credRootDomain = credDomainParts.slice(-2).join('.');
const currentRootDomain = currentDomainParts.slice(-2).join('.');
// Compare protocols and root domains
return credUrlObject.protocol === currentUrlObject.protocol &&
credRootDomain === currentRootDomain;
if (credUrlObject.protocol === currentUrlObject.protocol &&
credRootDomain === currentRootDomain) {
hasBaseUrlMatch = true;
}
} catch {
return false;
// Invalid URL, skip
}
}));
// 3. Page title word match if still no matches
if (filtered.length === 0 && pageTitle.length > 0) {
const titleWords = pageTitle.toLowerCase()
.split(/\s+/)
.filter(word =>
word.length > 3 && // Filter out words shorter than 4 characters
!CombinedStopWords.has(word.toLowerCase()) // Filter out generic words
);
// Check page title match
if (titleWords.length > 0) {
hasTitleMatch = titleWords.some(word => cred.ServiceName.toLowerCase().includes(word));
}
filtered = credentials.filter(cred =>
titleWords.some(word =>
cred.ServiceName.toLowerCase().includes(word)
)
);
}
// Ensure we have unique credentials
const uniqueCredentials = Array.from(new Map(filtered.map(cred => [cred.Id, cred])).values());
// Assign priority based on matches
if (hasBaseUrlMatch && hasTitleMatch) {
filtered.push({ ...cred, priority: 2 });
} else if (hasBaseUrlMatch) {
filtered.push({ ...cred, priority: 3 });
} else if (hasTitleMatch) {
filtered.push({ ...cred, priority: 4 });
}
});
// Sort by priority and then take unique credentials
const uniqueCredentials = Array.from(
new Map(filtered
.sort((a, b) => a.priority - b.priority)
.map(cred => [cred.Id, cred]))
.values()
);
// Show max 3 results
return uniqueCredentials.slice(0, 3);
}

View File

@@ -11,6 +11,7 @@ import { PasswordSettingsResponse } from '@/utils/types/messaging/PasswordSettin
import SqliteClient from '../../utils/SqliteClient';
import { BaseIdentityGenerator } from '@/utils/generators/Identity/implementations/base/BaseIdentityGenerator';
import { StringResponse } from '@/utils/types/messaging/StringResponse';
import { Credential } from '@/utils/types/Credential';
// TODO: store generic setting constants somewhere else.
export const DISABLED_SITES_KEY = 'local:aliasvault_disabled_sites';