/** * Host-aware URL pattern matching for Elly block/allow lists. * Prefer this over bare `url.includes(pattern)` for http(s) hosts. */ /** * @param {string} url * @returns {string} */ export function extractHostname(url) { try { return new URL(url).hostname.toLowerCase(); } catch { return ''; } } /** * Strip leading www. for domain comparisons. * @param {string} host * @returns {string} */ export function bareHostname(host) { return String(host || '').toLowerCase().replace(/^www\./, ''); } /** * True when host matches domain or is a subdomain of domain. * @param {string} host * @param {string} domain * @returns {boolean} */ export function hostMatchesDomain(host, domain) { const h = bareHostname(host); const d = bareHostname(domain); if (!h || !d) { return false; } return h === d || h.endsWith(`.${d}`); } /** * Match a single block/allow pattern against a URL. * Supports: * - Exact host / domain (e.g. example.com, www.example.com) * - `*.example.com` → host === example.com OR host.endsWith('.example.com') * - Scheme patterns like `chrome://extensions` via startsWith (careful, not bare includes for hosts) * - Pathful https patterns when pattern includes `/` * @param {string} url * @param {string} pattern * @returns {boolean} */ export function matchesUrlPattern(url, pattern) { if (!url || !pattern) { return false; } const urlStr = String(url); const patternStr = String(pattern).trim(); if (!patternStr) { return false; } const lowerUrl = urlStr.toLowerCase(); const lowerPat = patternStr.toLowerCase(); // Internal / scheme-prefixed patterns (chrome://, opera://, edge://, about:) if ( lowerPat.startsWith('chrome://') || lowerPat.startsWith('opera://') || lowerPat.startsWith('edge://') || lowerPat.startsWith('about:') ) { return lowerUrl.startsWith(lowerPat) || lowerUrl.includes(lowerPat); } // Wildcard host patterns: *.example.com if (patternStr.startsWith('*.')) { const domain = patternStr.slice(2).toLowerCase().replace(/\/.*$/, ''); try { const host = extractHostname(urlStr); if (!host) { return false; } return hostMatchesDomain(host, domain); } catch { return false; } } // Full URL or path-containing pattern if (patternStr.includes('://') || patternStr.includes('/')) { if (lowerUrl.startsWith(lowerPat) || lowerUrl.includes(lowerPat)) { return true; } // Fall through to host match on the hostname portion when parseable } // Host / domain pattern (no bare url.includes for google.com-style hosts) try { const host = extractHostname(urlStr); if (!host) { return lowerUrl.includes(lowerPat); } let domainPart = lowerPat; if (domainPart.includes('://')) { try { domainPart = new URL(patternStr).hostname.toLowerCase(); } catch { domainPart = lowerPat.replace(/^https?:\/\//, '').split('/')[0]; } } else if (domainPart.includes('/')) { domainPart = domainPart.split('/')[0]; } domainPart = domainPart.replace(/^\*\./, ''); return hostMatchesDomain(host, domainPart); } catch { return false; } } /** * @param {string} url * @param {string[] | null | undefined} list * @returns {boolean} */ export function isUrlInList(url, list) { if (!url || !list || !Array.isArray(list)) { return false; } return list.some((pattern) => matchesUrlPattern(url, pattern)); }