mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
Move "No filtering" section in "Settings" to "Develop" pane. It is now possible to view/edit the list of hostnames for any of the filtering mode. This takes care of these issues: - https://github.com/uBlockOrigin/uBOL-home/issues/270 - https://github.com/uBlockOrigin/uBOL-home/issues/297 Add ability to see all rulesets (read-only), to assist in investigating filtering issues.
581 lines
18 KiB
JavaScript
581 lines
18 KiB
JavaScript
/*******************************************************************************
|
|
|
|
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
|
|
Copyright (C) 2014-present Raymond Hill
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
|
|
|
Home: https://github.com/gorhill/uBlock
|
|
*/
|
|
|
|
/******************************************************************************/
|
|
|
|
const validActionValues = [
|
|
'block',
|
|
'redirect',
|
|
'allow',
|
|
'upgradeScheme',
|
|
'modifyHeaders',
|
|
'allowAllRequests',
|
|
];
|
|
|
|
const validBoolValues = [
|
|
'false',
|
|
'true',
|
|
];
|
|
|
|
const validHeaderOpValues = [
|
|
'append',
|
|
'remove',
|
|
'set',
|
|
];
|
|
|
|
const validDomainTypeValues = [
|
|
'firstParty',
|
|
'thirdParty',
|
|
];
|
|
|
|
const validRequestMethodValues = [
|
|
'connect',
|
|
'delete',
|
|
'get',
|
|
'head',
|
|
'options',
|
|
'patch',
|
|
'post',
|
|
'put',
|
|
'other',
|
|
];
|
|
|
|
const validResourceTypeValues = [
|
|
'main_frame',
|
|
'sub_frame',
|
|
'stylesheet',
|
|
'script',
|
|
'image',
|
|
'font',
|
|
'object',
|
|
'xmlhttprequest',
|
|
'ping',
|
|
'csp_report',
|
|
'media',
|
|
'websocket',
|
|
'webtransport',
|
|
'webbundle',
|
|
'other',
|
|
];
|
|
|
|
/******************************************************************************/
|
|
|
|
function selectParser(scope, rule, node) {
|
|
const parser = perScopeParsers[scope.join('.')];
|
|
if ( parser === undefined ) { return false; }
|
|
return parser(scope, rule, node);
|
|
}
|
|
|
|
const perScopeParsers = {
|
|
'': function(scope, rule, node) {
|
|
const { key, val } = node;
|
|
switch ( key ) {
|
|
case 'action':
|
|
case 'condition':
|
|
if ( val !== undefined ) { return false; }
|
|
rule[key] = {};
|
|
scope.push(key);
|
|
break;
|
|
case 'id': {
|
|
const n = parseInt(val, 10);
|
|
if ( isNaN(n) || n < 1) { return false; }
|
|
rule.id = n;
|
|
break;
|
|
}
|
|
case 'priority': {
|
|
const n = parseInt(val, 10);
|
|
if ( isNaN(n) || n < 1 ) { return false; }
|
|
rule.priority = n;
|
|
break;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
'action': function(scope, rule, node) {
|
|
const { key, val } = node;
|
|
switch ( key ) {
|
|
case 'type':
|
|
if ( validActionValues.includes(val) === false ) { return false; }
|
|
rule.action.type = val;
|
|
break;
|
|
case 'redirect':
|
|
rule.action.redirect = {};
|
|
scope.push('redirect');
|
|
break;
|
|
case 'requestHeaders':
|
|
case 'responseHeaders':
|
|
rule.action[key] = [];
|
|
scope.push(key);
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
'action.redirect': function(scope, rule, node) {
|
|
const { key, val } = node;
|
|
switch ( key ) {
|
|
case 'extensionPath':
|
|
case 'regexSubstitution':
|
|
case 'url':
|
|
rule.action.redirect[key] = val;
|
|
break;
|
|
case 'transform':
|
|
rule.action.redirect.transform = {};
|
|
scope.push('transform');
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
'action.redirect.transform': function(scope, rule, node) {
|
|
const { key, val } = node;
|
|
switch ( key ) {
|
|
case 'fragment':
|
|
case 'host':
|
|
case 'path':
|
|
case 'port':
|
|
case 'query':
|
|
case 'scheme': {
|
|
if ( val === undefined ) { return false; }
|
|
rule.action.redirect.transform[key] = val;
|
|
break;
|
|
}
|
|
case 'queryTransform':
|
|
rule.action.redirect.transform.queryTransform = {};
|
|
scope.push('queryTransform');
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
'action.redirect.transform.queryTransform': function(scope, rule, node) {
|
|
const { key, val } = node;
|
|
if ( val !== undefined ) { return false; }
|
|
switch ( key ) {
|
|
case 'addOrReplaceParams':
|
|
case 'removeParams':
|
|
rule.action.redirect.transform.queryTransform[key] = [];
|
|
scope.push(key);
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
'action.redirect.transform.queryTransform.addOrReplaceParams': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
rule.action.redirect.transform.queryTransform.addOrReplaceParams.push({});
|
|
scope.push('@');
|
|
return selectParser(scope, rule, node);
|
|
},
|
|
'action.redirect.transform.queryTransform.addOrReplaceParams.@': function(scope, rule, node) {
|
|
const { key, val } = node;
|
|
if ( val === undefined ) { return false; }
|
|
const item = rule.action.redirect.transform.queryTransform.addOrReplaceParams.at(-1);
|
|
switch ( key ) {
|
|
case 'key':
|
|
case 'value':
|
|
item[key] = val;
|
|
break;
|
|
case 'replaceOnly':
|
|
if ( validBoolValues.includes(val) === false ) { return false; }
|
|
item.replaceOnly = val === 'true';
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
'action.redirect.transform.queryTransform.removeParams': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
rule.action.redirect.transform.queryTransform.removeParams.push(node.val);
|
|
return true;
|
|
},
|
|
'action.requestHeaders': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
rule.action.requestHeaders.push({});
|
|
scope.push('@');
|
|
return selectParser(scope, rule, node);
|
|
},
|
|
'action.requestHeaders.@': function(scope, rule, node) {
|
|
const { key, val } = node;
|
|
const item = rule.action.requestHeaders.at(-1);
|
|
switch ( key ) {
|
|
case 'header':
|
|
case 'value':
|
|
item[key] = val;
|
|
break;
|
|
case 'operation':
|
|
if ( validHeaderOpValues.includes(val) === false ) { return false; }
|
|
item.operation = val;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
'action.responseHeaders': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
rule.action.responseHeaders.push({});
|
|
scope.push('@');
|
|
return selectParser(scope, rule, node);
|
|
},
|
|
'action.responseHeaders.@': function(scope, rule, node) {
|
|
const { key, val } = node;
|
|
const item = rule.action.responseHeaders.at(-1);
|
|
switch ( key ) {
|
|
case 'header':
|
|
case 'value':
|
|
item[key] = val;
|
|
break;
|
|
case 'operation':
|
|
if ( validHeaderOpValues.includes(val) === false ) { return false; }
|
|
item.operation = val;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
'condition': function(scope, rule, node) {
|
|
const { key, val } = node;
|
|
switch ( key ) {
|
|
case 'domainType':
|
|
if ( validDomainTypeValues.includes(val) === false ) { return false; }
|
|
rule.condition.domainType = val;
|
|
break;
|
|
case 'isUrlFilterCaseSensitive':
|
|
if ( validBoolValues.includes(val) === false ) { return false; }
|
|
rule.condition.isUrlFilterCaseSensitive = val === 'true';
|
|
break;
|
|
case 'regexFilter':
|
|
case 'urlFilter':
|
|
if ( val === undefined ) { return false; }
|
|
rule.condition[key] = val;
|
|
break;
|
|
case 'initiatorDomains':
|
|
case 'excludedInitiatorDomains':
|
|
case 'requestDomains':
|
|
case 'excludedRequestDomains':
|
|
case 'resourceTypes':
|
|
case 'excludedResourceTypes':
|
|
case 'requestMethods':
|
|
case 'excludedRequestMethods':
|
|
case 'responseHeaders':
|
|
case 'excludedResponseHeaders':
|
|
rule.condition[key] = [];
|
|
scope.push(key);
|
|
break;
|
|
case 'tabIds':
|
|
rule.condition.tabIds = [];
|
|
scope.push('tabIds');
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
'condition.initiatorDomains': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
rule.condition.initiatorDomains.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.excludedInitiatorDomains': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
rule.condition.excludedInitiatorDomains.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.domains': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
rule.condition.domains.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.excludedDomains': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
rule.condition.excludedDomains.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.requestDomains': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
rule.condition.requestDomains.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.excludedRequestDomains': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
rule.condition.excludedRequestDomains.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.resourceTypes': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
if ( validResourceTypeValues.includes(node.val) === false ) { return false; }
|
|
rule.condition.resourceTypes.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.excludedResourceTypes': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
if ( validResourceTypeValues.includes(node.val) === false ) { return false; }
|
|
rule.condition.excludedResourceTypes.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.requestMethods': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
if ( validRequestMethodValues.includes(node.val) === false ) { return false; }
|
|
rule.condition.requestMethods.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.excludedRequestMethods': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
if ( validRequestMethodValues.includes(node.val) === false ) { return false; }
|
|
rule.condition.excludedRequestMethods.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.responseHeaders': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
rule.condition.responseHeaders.push({});
|
|
scope.push('@');
|
|
return selectParser(scope, rule, node);
|
|
},
|
|
'condition.responseHeaders.@': function(scope, rule, node) {
|
|
const item = rule.condition.responseHeaders.at(-1);
|
|
switch ( node.key ) {
|
|
case 'header':
|
|
if ( node.val === undefined ) { return false; }
|
|
item.header = node.val;
|
|
break;
|
|
case 'values':
|
|
case 'excludedValues':
|
|
item[node.key] = [];
|
|
scope.push(node.key);
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
'condition.responseHeaders.@.values': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
const item = rule.condition.responseHeaders.at(-1);
|
|
item.values.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.responseHeaders.@.excludedValues': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
const item = rule.condition.responseHeaders.at(-1);
|
|
item.excludedValues.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.excludedResponseHeaders': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
rule.condition.excludedResponseHeaders.push({});
|
|
scope.push('@');
|
|
return selectParser(scope, rule, node);
|
|
},
|
|
'condition.excludedResponseHeaders.@': function(scope, rule, node) {
|
|
const item = rule.condition.excludedResponseHeaders.at(-1);
|
|
switch ( node.key ) {
|
|
case 'header':
|
|
if ( node.val === undefined ) { return false; }
|
|
item.header = node.val;
|
|
break;
|
|
case 'values':
|
|
case 'excludedValues':
|
|
item[node.key] = [];
|
|
scope.push(node.key);
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
'condition.excludedResponseHeaders.@.values': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
const item = rule.condition.excludedResponseHeaders.at(-1);
|
|
item.values.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.excludedResponseHeaders.@.excludedValues': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
const item = rule.condition.excludedResponseHeaders.at(-1);
|
|
item.excludedValues.push(node.val);
|
|
return true;
|
|
},
|
|
'condition.tabIds': function(scope, rule, node) {
|
|
if ( node.list !== true ) { return false; }
|
|
const n = parseInt(node.val, 10);
|
|
if ( isNaN(n) || n === 0 ) { return false; }
|
|
rule.condition.tabIds.push(n);
|
|
},
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
function depthFromIndent(line) {
|
|
const match = /^\s*/.exec(line);
|
|
const count = match[0].length;
|
|
if ( (count & 1) !== 0 ) { return -1; }
|
|
return count / 2;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function nodeFromLine(line) {
|
|
const match = reNodeParser.exec(line);
|
|
const out = {};
|
|
if ( match === null ) { return out; }
|
|
if ( match[1] ) {
|
|
out.list = true;
|
|
}
|
|
if ( match[4] ) {
|
|
out.val = match[4].trim();
|
|
} else if ( match[3] ) {
|
|
out.key = match[2];
|
|
out.val = match[3].trim();
|
|
if ( out.val === "''" ) { out.val = '' };
|
|
} else {
|
|
out.key = match[2];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const reNodeParser = /^\s*(- )?(?:(\S+):( \S.*)?|(\S.*))$/;
|
|
|
|
/******************************************************************************/
|
|
|
|
function ruleFromLines(lines, indices) {
|
|
const rule = {};
|
|
const bad = [];
|
|
const scope = [];
|
|
for ( const i of indices ) {
|
|
const line = lines[i];
|
|
const depth = depthFromIndent(line);
|
|
if ( depth < 0 ) {
|
|
bad.push(i);
|
|
continue;
|
|
}
|
|
scope.length = depth;
|
|
const node = nodeFromLine(line);
|
|
const result = selectParser(scope, rule, node);
|
|
if ( result === false ) {
|
|
bad.push(i);
|
|
}
|
|
}
|
|
if ( bad.length !== 0 ) { return { bad }; }
|
|
return { rule };
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
export function rulesFromText(text) {
|
|
const rules = [];
|
|
const bad = [];
|
|
const lines = [ ...text.split(/\n\r|\r\n|\n|\r/), '---' ];
|
|
const indices = [];
|
|
for ( let i = 0; i < lines.length; i++ ) {
|
|
const line = lines[i].trimEnd();
|
|
if ( line.trim().startsWith('#') ) { continue; }
|
|
if ( line !== '---' && line !== '...' ) {
|
|
indices.push(i);
|
|
continue;
|
|
}
|
|
// Discard leading empty lines
|
|
while ( indices.length !== 0 ) {
|
|
const s = lines[indices[0]].trim();
|
|
if ( s.length !== 0 ) { break; }
|
|
indices.shift();
|
|
}
|
|
// Discard trailing empty lines
|
|
while ( indices.length !== 0 ) {
|
|
const s = lines[indices.at(-1)].trim();
|
|
if ( s.length !== 0 ) { break; }
|
|
indices.pop();
|
|
}
|
|
if ( indices.length === 0 ) { continue; }
|
|
const result = ruleFromLines(lines, indices);
|
|
if ( result.bad ) {
|
|
bad.push(...result.bad);
|
|
} else if ( result.rule ) {
|
|
rules.push(result.rule);
|
|
}
|
|
indices.length = 0;
|
|
}
|
|
return { rules, bad };
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function textFromValue(val, depth) {
|
|
const indent = ' '.repeat(depth);
|
|
switch ( typeof val ) {
|
|
case 'boolean':
|
|
case 'number':
|
|
return `${val}`;
|
|
case 'string':
|
|
if ( val === '' ) { return "''"; }
|
|
return val;
|
|
}
|
|
const out = [];
|
|
if ( Array.isArray(val) ) {
|
|
for ( const a of val ) {
|
|
const s = textFromValue(a, depth+1);
|
|
if ( s === undefined ) { continue; }
|
|
out.push(`${indent}- ${s.trimStart()}`);
|
|
}
|
|
return out.join('\n');
|
|
}
|
|
if ( val instanceof Object ) {
|
|
for ( const [ a, b ] of Object.entries(val) ) {
|
|
const s = textFromValue(b, depth+1);
|
|
if ( s === undefined ) { continue; }
|
|
if ( b instanceof Object ) {
|
|
out.push(`${indent}${a}:\n${s}`);
|
|
} else {
|
|
out.push(`${indent}${a}: ${s}`);
|
|
}
|
|
}
|
|
return out.join('\n');
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
export function textFromRules(rules, option = {}) {
|
|
if ( Array.isArray(rules) === false ) {
|
|
if ( rules instanceof Object === false ) { return; }
|
|
rules = [ rules ];
|
|
}
|
|
const out = [];
|
|
for ( const rule of rules ) {
|
|
if ( option.keepId !== true && rule.id ) { rule.id = undefined };
|
|
const text = textFromValue(rule, 0);
|
|
if ( text === undefined ) { continue; }
|
|
out.push(text, '---' );
|
|
}
|
|
if ( out.length !== 0 ) {
|
|
out.unshift('---');
|
|
out.push('');
|
|
}
|
|
return out.join('\n');
|
|
}
|