// Copyright (C) 2023-2025 IITC-CE - GPL-3.0 with Store Exception - see LICENSE and COPYING.STORE
const CACHE = {};
const RE_URL = /(.*?):\/\/([^/]*)\/(.*)/;
/**
* Checks the URL for match/include plugin.
*
* @param {plugin} meta - Object with data from ==UserScript== header.
* @param {string} url - Page URL.
* @return {boolean}
*/
export function check_matching(meta, url) {
const match = meta.match || [];
const include = meta.include || [];
const match_exclude = meta['exclude-match'] || [];
const exclude = meta.exclude || [];
// match all if no @match or @include rule and set url === '<all_ingress>'
let ok = !match.length && !include.length && url === '<all_ingress>';
// @match
ok = ok || testMatch(url, match);
// @include
ok = ok || testInclude(url, include);
// @exclude-match
ok = ok && !testMatch(url, match_exclude);
// @exclude
ok = ok && !testInclude(url, exclude);
return ok;
}
function str2RE(str) {
const re = str.replace(/([.?/])/g, '\\$1').replace(/\*/g, '.*?');
return RegExp(`^${re}$`);
}
/**
* Test glob rules like `@include` and `@exclude`.
*/
export function testInclude(url, rules) {
return rules.some((rule) => {
const key = `re:${rule}`;
let re = CACHE[key];
if (!re) {
re = makeIncludeRegExp(rule);
CACHE[key] = re;
}
return re.test(url);
});
}
function makeIncludeRegExp(str) {
if (str.length > 1 && str[0] === '/' && str[str.length - 1] === '/') {
return RegExp(str.slice(1, -1)); // Regular-expression
}
return str2RE(str); // Wildcard
}
/**
* Test match rules like `@match` and `@exclude_match`.
*/
export function testMatch(url, rules) {
return rules.some((rule) => {
const key = `match:${rule}`;
let matcher = CACHE[key];
if (!matcher) {
matcher = makeMatchRegExp(rule);
CACHE[key] = matcher;
}
return matcher.test(url);
});
}
function makeMatchRegExp(rule) {
let test;
if (rule === '<all_urls>') test = () => true;
else {
const ruleParts = rule.match(RE_URL);
test = (url) => {
const parts = url.match(RE_URL);
return !!ruleParts && !!parts && matchScheme(ruleParts[1], parts[1]) && matchHost(ruleParts[2], parts[2]) && matchPath(ruleParts[3], parts[3]);
};
}
return { test };
}
function matchScheme(rule, data) {
// exact match
if (rule === data) return 1;
// * = http | https
if (rule === '*' && /^https?$/i.test(data)) return 1;
return 0;
}
function matchHost(rule, data) {
// * matches all
if (rule === '*') return 1;
// exact match
if (rule === data) return 1;
// *.example.com
if (/^\*\.[^*]*$/.test(rule)) {
// matches the specified domain
if (rule.slice(2) === data) return 1;
// matches subdomains
if (str2RE(rule).test(data)) return 1;
}
return 0;
}
function matchPath(rule, data) {
return str2RE(rule).test(data);
}
/**
* Returns information about the domains for which the script will be enabled.
* Returns null if @match and @include are not specified
* Returns <all_urls> if the script will be run for all domains. Additional URL Scheme and Path filters are not taken into account.
* Otherwise, it returns a list of strings with domains.
*
* @param {plugin} meta - Object with data from ==UserScript== header.
* @return {null|string|Array.<string>}
*/
export function humanize_match(meta) {
const match = meta.match || [];
const include = meta.include || [];
const matches = match.concat(include);
if (!matches.length) return null;
if (matches.includes('<all_urls>')) return '<all_urls>';
const domains = [];
for (const item of matches) {
const parts = item.match(RE_URL);
if (!parts) continue;
const [, , domain] = parts;
if (domain === '*') return '<all_urls>';
if (!domains.includes(domain)) domains.push(domain);
}
return domains;
}