email-scrubber-core
Version:
A privacy-focused email sanitizer that removes trackers and cleans URLs, powered by the ClearURLs ruleset.
193 lines • 7.96 kB
JavaScript
/**
* A class for cleaning URLs by removing tracking parameters, based on the ClearURLs ruleset.
* All regex patterns are compiled eagerly in the constructor for optimal runtime performance.
*/
export class LinkCleaner {
rules;
cachedProviders;
/**
* Creates an instance of the LinkCleaner.
* @param rules The ClearURLs rules data.
*/
constructor(rules) {
this.rules = rules;
this.cachedProviders = new Map();
// Eagerly compile all providers' regex patterns
for (const [provider, providerRules] of Object.entries(rules.providers)) {
const cachedProvider = this.compileProvider(providerRules, provider);
this.cachedProviders.set(provider, cachedProvider);
}
}
/**
* Gets a cached provider's rules.
* @param provider The provider identifier to get rules for.
* @returns The cached provider rule or null if not found.
*/
getProvider(provider) {
return this.cachedProviders.get(provider) || null;
}
/**
* Compiles a provider's rules into cached regex patterns.
* All patterns are compiled eagerly.
* @param providerRules The raw provider rule.
* @param provider The provider identifier for error reporting.
* @returns Compiled provider rule with regex patterns.
*/
compileProvider(providerRules, provider) {
return {
urlPattern: this.compileUrlPattern(providerRules.urlPattern, provider),
rules: this.compileCombinedPattern(providerRules.rules, provider, "rules"),
exceptions: this.compileCombinedPattern(providerRules.exceptions || [], provider, "exceptions"),
redirections: this.compileCombinedPattern(providerRules.redirections || [], provider, "redirections"),
referral: this.compileCombinedPattern(providerRules.referral || [], provider, "referral"),
};
}
/**
* Compiles a URL pattern regex, with error handling.
* @param pattern The regex pattern string.
* @param provider The provider identifier for error reporting.
* @returns Compiled RegExp or null if invalid.
*/
compileUrlPattern(pattern, provider) {
try {
return new RegExp(pattern, "i");
}
catch (error) {
console.error(`Invalid URL regex for provider ${provider}: ${pattern}`, error);
return null;
}
}
/**
* Compiles an array of pattern strings into a single combined RegExp object.
* @param patterns Array of regex pattern strings.
* @param provider The provider identifier for error reporting.
* @param fieldName The field name for error reporting.
* @returns A single compiled RegExp object, or null if no patterns are provided.
*/
compileCombinedPattern(patterns, provider, fieldName) {
if (patterns.length === 0) {
return null;
}
try {
// Combine all patterns into a single regex: (pattern1|pattern2|...)
const combined = `(?:${patterns.join("|")})`;
return new RegExp(combined, "i");
}
catch (error) {
console.error(`Invalid ${fieldName} regex for provider ${provider}: ${patterns.join(", ")}`, error);
return null;
}
}
/**
* Cleans a URL by removing tracking parameters and resolving redirection links.
*
* @param url The URL to be cleaned. It can be a string or a URL object.
* @returns A cleaned URL object.
*/
clean(url) {
let urlObject;
try {
urlObject = typeof url === "string" ? new URL(url) : url;
}
catch (_e) {
// If the URL is invalid, we cannot process it.
// Return the original input if it was a URL object, or throw if it was an invalid string.
if (url instanceof URL) {
return url;
}
throw new Error(`Invalid URL provided: ${url}`);
}
// Early exit if there are no query parameters to clean.
if (urlObject.search.length === 0) {
return urlObject;
}
// Apply the universal default rules first, if they exist.
const defaultProvider = this.getProvider("globalRules");
if (defaultProvider) {
this.cleanParams(urlObject, defaultProvider);
}
// Then, find and apply a more specific provider rule.
const specificProvider = this.findProvider(urlObject);
if (specificProvider) {
// Handle redirections first, as they often contain the final destination URL.
if (specificProvider.redirections) {
for (const param of Array.from(urlObject.searchParams.keys())) {
if (this.matchesPattern(param, specificProvider.redirections)) {
const redirectUrl = urlObject.searchParams.get(param);
if (redirectUrl) {
try {
// The redirected URL should also be cleaned.
return this.clean(decodeURIComponent(redirectUrl));
}
catch (_e) {
// The redirect parameter was not a valid URL, so we ignore it.
}
}
}
}
}
// Clean the parameters of the current URL using the specific provider's rules.
this.cleanParams(urlObject, specificProvider);
}
return urlObject;
}
/**
* Finds the matching provider rule for a given URL.
* @param urlObject The URL to find a provider for.
* @returns The matching cached provider rule, or null if no provider matches.
*/
findProvider(urlObject) {
for (const provider of Object.keys(this.rules.providers)) {
// Skip the default provider in this lookup, as it's handled separately.
if (provider === "globalRules") {
continue;
}
// Get pre-compiled provider
const cachedProvider = this.getProvider(provider);
if (cachedProvider?.urlPattern?.test(urlObject.href)) {
// If a provider matches, check if the URL is an exception.
if (this.matchesPattern(urlObject.href, cachedProvider.exceptions)) {
// This URL is an exception for this provider, so skip it.
continue;
}
return cachedProvider;
}
}
return null;
}
/**
* Removes unwanted query parameters from a URL based on a cached provider's rules.
* @param urlObject The URL object to be modified.
* @param cachedProvider The cached provider rule to apply.
*/
cleanParams(urlObject, cachedProvider) {
// Early exit if the provider has no rules to apply.
if (!cachedProvider.rules && !cachedProvider.referral) {
return;
}
const paramsToDelete = [];
for (const param of urlObject.searchParams.keys()) {
const shouldRemove = this.matchesPattern(param, cachedProvider.rules) ||
this.matchesPattern(param, cachedProvider.referral);
if (shouldRemove) {
paramsToDelete.push(param);
}
}
for (const param of paramsToDelete) {
urlObject.searchParams.delete(param);
}
}
/**
* Tests if a string matches a compiled regex pattern.
* @param value The string to test.
* @param pattern The compiled RegExp object.
* @returns True if the string matches the pattern.
*/
matchesPattern(value, pattern) {
if (!pattern) {
return false;
}
return pattern.test(value);
}
}
//# sourceMappingURL=LinkCleaner.js.map