1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-09-14 23:12:28 +02:00

dynamic filtering

This commit is contained in:
gorhill 2014-10-06 14:02:44 -04:00
parent 4227123393
commit 9bcad432cf
11 changed files with 476 additions and 24 deletions

View File

@ -5,7 +5,7 @@ body {
font: 13px sans-serif;
background-color: white;
direction: __MSG_@@bidi_dir__;
min-width: 160px;
min-width: 180px;
}
h1,h2,h3,h4 {
margin: 0;
@ -58,6 +58,7 @@ p {
}
#total-blocked {
margin-top: 4px;
margin-bottom: 8px;
font-size: 14px;
}
#stats {
@ -76,3 +77,110 @@ p {
.tool:hover {
color: #444;
}
#dynamicFilteringToggler {
margin: 0;
border: 0;
padding: 0;
width: 100vw;
text-align: center;
cursor: pointer;
}
#dynamicFilteringToggler::after {
content: '\f107';
}
#dynamicFilteringToggler.on::after {
content: '\f106';
}
#dynamicFilteringContainer {
margin: 0;
border: 0;
padding: 0;
display: none;
}
#dynamicFilteringToggler.on + #dynamicFilteringContainer {
display: initial;
}
.dynamicFiltering {
margin: 0;
border: 0;
padding: 0;
height: 2.75em;
position: relative;
}
.dynamicFiltering.local {
border-bottom: 1px solid white;
}
.dynamicFiltering.global {
height: 1.25em;
}
.dynamicFiltering > div {
margin: 0;
border: 1px solid #eee;
padding: 0;
box-sizing: border-box;
background-color: #eee;
position: absolute;
cursor: pointer;
}
.dynamicFiltering > div:not(:first-child) {
border-left: 1px solid white !important;
}
.dynamicFiltering > div:nth-of-type(1) {
left: 0;
width: 7vw;
height: 100%;
}
.dynamicFiltering > div:nth-of-type(2) {
left: 7vw;
width: 18vw;
height: 100%;
}
.dynamicFiltering > div:nth-of-type(3) {
left: 25vw;
width: 25vw;
height: 100%;
}
.dynamicFiltering > div:nth-of-type(4) {
left: 50vw;
width: 25vw;
height: 100%;
}
.dynamicFiltering > div:nth-of-type(5) {
left: 75vw;
width: 25vw;
height: 100%;
}
.dynamicFiltering > div:nth-of-type(6) {
left: 0;
width: 50vw;
}
.dynamicFiltering > div:nth-of-type(7) {
left: 50vw;
width: 50vw;
}
.dynamicFiltering > div.label {
margin: 0;
border: 0;
padding: 0;
pointer-events: none;
color: black;
opacity: 0.2;
font: 12px monospace;
text-align: center;
top: 50%;
transform: translateY(-50%);
background-color: transparent;
}
.dynamicFiltering > div.blocked {
border-color: #fdd;
background-color: #fdd;
}
.dynamicFiltering > div.ownFilter {
border-color: #bbb;
background-color: #bbb;
}
.dynamicFiltering > div.blocked.ownFilter {
border-color: #eaa;
background-color: #eaa;
}

View File

@ -55,6 +55,8 @@ return {
autoUpdate: true,
collapseBlocked: true,
contextMenuEnabled: true,
dynamicFilteringSelfie: '',
dynamicFilteringEnabled: false,
experimentalEnabled: false,
externalLists: defaultExternalLists,
logRequests: false,

View File

@ -34,6 +34,18 @@ var µb = µBlock;
/******************************************************************************/
var getDynamicFilterResults = function(scope) {
return [
µb.netFilteringEngine.matchDynamicFilters(scope, 'inline-script', true),
µb.netFilteringEngine.matchDynamicFilters(scope, 'script', true),
µb.netFilteringEngine.matchDynamicFilters(scope, 'script', false),
µb.netFilteringEngine.matchDynamicFilters(scope, 'sub_frame', true),
µb.netFilteringEngine.matchDynamicFilters(scope, 'sub_frame', false)
];
};
/******************************************************************************/
var getStats = function(request) {
var r = {
globalBlockedRequestCount: µb.localSettings.blockedRequestCount,
@ -44,7 +56,11 @@ var getStats = function(request) {
pageAllowedRequestCount: 0,
netFilteringSwitch: false,
cosmeticFilteringSwitch: false,
logRequests: µb.userSettings.logRequests
logRequests: µb.userSettings.logRequests,
dynamicFilteringEnabled: µb.userSettings.dynamicFilteringEnabled,
dynamicFilterResults: {
'/': getDynamicFilterResults('*')
}
};
var pageStore = µb.pageStoreFromTabId(request.tabId);
if ( pageStore ) {
@ -53,6 +69,7 @@ var getStats = function(request) {
r.pageBlockedRequestCount = pageStore.perLoadBlockedRequestCount;
r.pageAllowedRequestCount = pageStore.perLoadAllowedRequestCount;
r.netFilteringSwitch = pageStore.getNetFilteringSwitch();
r.dynamicFilterResults['.'] = getDynamicFilterResults(r.pageHostname);
}
return r;
};
@ -70,6 +87,13 @@ var onMessage = function(request, sender, callback) {
var response;
switch ( request.what ) {
case 'gotoPick':
// Picker launched from popup: clear context menu args
µb.contextMenuClientX = -1;
µb.contextMenuClientY = -1;
µb.elementPickerExec(request.tabId);
break;
case 'stats':
response = getStats(request);
break;
@ -83,11 +107,12 @@ var onMessage = function(request, sender, callback) {
µb.updateBadgeAsync(request.tabId);
break;
case 'gotoPick':
// Picker launched from popup: clear context menu args
µb.contextMenuClientX = -1;
µb.contextMenuClientY = -1;
µb.elementPickerExec(request.tabId);
case 'toggleDynamicFilter':
µb.toggleDynamicFilter(request);
response = { '/': getDynamicFilterResults('*') };
if ( request.pageHostname ) {
response['.'] = getDynamicFilterResults(request.pageHostname);
}
break;
default:

View File

@ -66,18 +66,14 @@ var typeNameToTypeValue = {
};
const BlockAnyTypeAnyParty = BlockAction | AnyType | AnyParty;
const BlockAnyType1stParty = BlockAction | AnyType | FirstParty;
const BlockAnyType3rdParty = BlockAction | AnyType | ThirdParty;
const BlockAnyType = BlockAction | AnyType;
const BlockAnyParty = BlockAction | AnyParty;
const AllowAnyTypeAnyParty = AllowAction | AnyType | AnyParty;
const AllowAnyType1stParty = AllowAction | AnyType | FirstParty;
const AllowAnyType3rdParty = AllowAction | AnyType | ThirdParty;
const AllowAnyType = AllowAction | AnyType;
const AllowAnyParty = AllowAction | AnyParty;
var pageHostname = '';
var pageHostname = ''; // short-lived register
var reIgnoreEmpty = /^\s+$/;
var reIgnoreComment = /^\[|^!/;
@ -842,7 +838,7 @@ FilterManyWildcardsHostname.fromSelfie = function(s) {
var FilterBucket = function(a, b) {
this.promoted = 0;
this.vip = 16;
this.f = null;
this.f = null; // short-lived register
this.filters = [];
if ( a !== undefined ) {
this.filters[0] = a;
@ -1107,7 +1103,7 @@ FilterParser.prototype.parseOptType = function(raw, not) {
// `popup` is a special type, it cannot be set for filters intended
// for real net request types. The test is safe since there is no
// such thing as a filter using `~popup`.
if ( typeNameToTypeValue[k] > typeNameToTypeValue['other'] ) {
if ( typeNameToTypeValue[k] > typeNameToTypeValue.other ) {
continue;
}
this.types.push(typeNameToTypeValue[k]);
@ -1296,6 +1292,7 @@ FilterContainer.prototype.reset = function() {
this.duplicates = Object.create(null);
this.blockedAnyPartyHostnames.reset();
this.blocked3rdPartyHostnames.reset();
this.dynamicFilters = {};
this.filterParser.reset();
};
@ -1633,6 +1630,190 @@ FilterContainer.prototype.addToCategory = function(category, tokenKey, filter) {
/******************************************************************************/
// Dynamic filters
// Bits:
// 0: inline script blocked
// 1: inline script allowed
// 2: first-party script blocked
// 3: first-party script allowed
// 4: third-party script blocked
// 5: third-party script allowed
// 6: first-party frame blocked
// 7: first-party frame allowed
// 8: third-party frame blocked
// 9: third-party frame allowed
//
// I chose separate bits for blocked/unblocked as I want to have an "undefined"
// state, which will be used to inherit from wider-scoped filters.
//
// undefined: 0x00
// blocked: 0x01
// allowed: 0x02
// unused: 0x03
FilterContainer.prototype.dynamicFilterBitOffsets = {};
FilterContainer.prototype.dynamicFilterBitOffsets[FirstParty | typeNameToTypeValue['inline-script']] = 0;
FilterContainer.prototype.dynamicFilterBitOffsets[FirstParty | typeNameToTypeValue.script] = 2;
FilterContainer.prototype.dynamicFilterBitOffsets[ThirdParty | typeNameToTypeValue.script] = 4;
FilterContainer.prototype.dynamicFilterBitOffsets[FirstParty | typeNameToTypeValue.sub_frame] = 6;
FilterContainer.prototype.dynamicFilterBitOffsets[ThirdParty | typeNameToTypeValue.sub_frame] = 8;
FilterContainer.prototype.dynamicFiltersMagicId = 'numrebvoacir';
/******************************************************************************/
FilterContainer.prototype.dynamicFilterSet = function(hostname, requestType, firstParty, value) {
if ( typeNameToTypeValue.hasOwnProperty(requestType) === false ) {
return false;
}
var party = firstParty ? FirstParty : ThirdParty;
var categoryKey = party | typeNameToTypeValue[requestType];
if ( this.dynamicFilterBitOffsets.hasOwnProperty(categoryKey) === false ) {
return false;
}
var bitOffset = this.dynamicFilterBitOffsets[categoryKey];
var oldFilter = this.dynamicFilters[hostname] || 0;
var newFilter = (oldFilter & ~(0x0003 << bitOffset)) | (value << bitOffset);
if ( newFilter === oldFilter ) {
return false;
}
if ( newFilter === 0 ) {
delete this.dynamicFilters[hostname];
} else {
this.dynamicFilters[hostname] = newFilter;
}
return true;
};
/******************************************************************************/
FilterContainer.prototype.dynamicFilterBlock = function(hostname, requestType, firstParty) {
if ( typeof hostname !== 'string' || hostname === '' ) {
return false;
}
var result = this.matchDynamicFilters(hostname, requestType, firstParty);
if ( result !== '' && result.slice(0, 2) !== '@@' ) {
return false;
}
this.dynamicFilterSet(hostname, requestType, firstParty, 0x00);
result = this.matchDynamicFilters(hostname, requestType, firstParty);
if ( result !== '' && result.slice(0, 2) !== '@@' ) {
return true;
}
this.dynamicFilterSet(hostname, requestType, firstParty, 0x01);
return true;
};
/******************************************************************************/
FilterContainer.prototype.dynamicFilterUnblock = function(hostname, requestType, firstParty) {
if ( typeof hostname !== 'string' || hostname === '' ) {
return false;
}
var result = this.matchDynamicFilters(hostname, requestType, firstParty);
if ( result === '' || result.slice(0, 2) === '@@' ) {
return false;
}
this.dynamicFilterSet(hostname, requestType, firstParty, 0x00);
result = this.matchDynamicFilters(hostname, requestType, firstParty);
if ( result === '' || result.slice(0, 2) === '@@' ) {
return true;
}
this.dynamicFilterSet(hostname, requestType, firstParty, 0x02);
return true;
};
/******************************************************************************/
FilterContainer.prototype.dynamicFilterStateToType = {
0x0001: '',
0x0002: '@@',
0x0004: '',
0x0008: '@@',
0x0010: '',
0x0020: '@@',
0x0040: '',
0x0080: '@@',
0x0100: '',
0x0200: '@@'
};
FilterContainer.prototype.dynamicFilterStateToOption = {
0x0001: '$inline-script,important',
0x0002: '$inline-script',
0x0004: '$~third-party,script,important',
0x0008: '$~third-party,script',
0x0010: '$third-party,script,important',
0x0020: '$third-party,script',
0x0040: '$~third-party,subdocument,important',
0x0080: '$~third-party,subdocument',
0x0100: '$third-party,subdocument,important',
0x0200: '$third-party,subdocument'
};
FilterContainer.prototype.matchDynamicFilters = function(hostname, requestType, firstParty) {
var party = firstParty ? FirstParty : ThirdParty;
if ( typeNameToTypeValue.hasOwnProperty(requestType) === false ) {
return '';
}
var categoryKey = party | typeNameToTypeValue[requestType];
if ( this.dynamicFilterBitOffsets.hasOwnProperty(categoryKey) === false ) {
return '';
}
var bitOffset = this.dynamicFilterBitOffsets[categoryKey];
var bitMask = 0x0003 << bitOffset;
var bitState, pos;
for ( ;; ) {
if ( this.dynamicFilters.hasOwnProperty(hostname) !== false ) {
bitState = this.dynamicFilters[hostname] & bitMask;
if ( bitState !== 0 ) {
if ( hostname !== '*' ) {
hostname = '||' + hostname + '^';
}
return this.dynamicFilterStateToType[bitState] +
hostname +
this.dynamicFilterStateToOption[bitState];
}
}
pos = hostname.indexOf('.');
if ( pos === -1 ) {
if ( hostname === '*' ) {
return '';
}
hostname = '*';
} else {
hostname = hostname.slice(pos + 1);
}
}
// unreachable
};
/******************************************************************************/
FilterContainer.prototype.selfieFromDynamicFilters = function() {
var bin = {
magicId: this.dynamicFiltersMagicId,
filters: this.dynamicFilters
};
return JSON.stringify(bin);
};
/******************************************************************************/
FilterContainer.prototype.dynamicFiltersFromSelfie = function(selfie) {
if ( selfie === '' ) {
return;
}
var bin = JSON.parse(selfie);
if ( bin.magicId !== this.dynamicFiltersMagicId ) {
return;
}
this.dynamicFilters = bin.filters;
};
/******************************************************************************/
// Since the addition of the `important` evaluation, this means it is now
// likely that the url will have to be scanned more than once. So this is
// to ensure we do it once only, and reuse results.
@ -1766,6 +1947,14 @@ FilterContainer.prototype.matchStringExactType = function(pageDetails, requestUR
var party = requestHostname.slice(-pageDomain.length) === pageDomain ?
FirstParty :
ThirdParty;
// Evaluate dynamic filters first. "Block" dynamic filters are always
// "important", they override everything else.
var bf = this.matchDynamicFilters(requestHostname, requestType, party === FirstParty);
if ( bf !== '' && bf.slice(0, 2) !== '@@' ) {
return bf;
}
var type = typeNameToTypeValue[requestType];
var categories = this.categories;
var buckets = this.buckets;
@ -1783,7 +1972,7 @@ FilterContainer.prototype.matchStringExactType = function(pageDetails, requestUR
// Test against important block filters
buckets[2] = categories[this.makeCategoryKey(BlockAnyParty | Important | type)];
buckets[3] = categories[this.makeCategoryKey(BlockAction | Important | type | party)];
var bf = this.matchTokens(url);
bf = this.matchTokens(url);
if ( bf !== false ) {
return bf.toString();
}
@ -1851,6 +2040,13 @@ FilterContainer.prototype.matchString = function(pageDetails, requestURL, reques
}
}
// Evaluate dynamic filters first. "Block" dynamic filters are always
// "important", they override everything else.
var bf = this.matchDynamicFilters(requestHostname, requestType, party === FirstParty);
if ( bf !== '' && bf.slice(0, 2) !== '@@' ) {
return bf;
}
// This will be used by hostname-based filters
pageHostname = pageDetails.pageHostname || '';
@ -1870,7 +2066,7 @@ FilterContainer.prototype.matchString = function(pageDetails, requestURL, reques
buckets[1] = categories[this.makeCategoryKey(BlockAnyType | Important | party)];
buckets[2] = categories[this.makeCategoryKey(BlockAnyParty | Important | type)];
buckets[3] = categories[this.makeCategoryKey(BlockAction | Important | type | party)];
var bf = this.matchTokens(url);
bf = this.matchTokens(url);
if ( bf !== false ) {
return bf.toString() + '$important';
}

View File

@ -425,6 +425,14 @@ PageStore.prototype.setRequestFlags = function(requestURL, targetBits, valueBits
/******************************************************************************/
PageStore.prototype.recordResult = function(requestType, requestURL, result) {
if ( collapsibleRequestTypes.indexOf(requestType) !== -1 || µb.userSettings.logRequests ) {
this.netFilteringCache.add(requestURL, result, requestType, 0);
}
};
/******************************************************************************/
// false: not blocked
// true: blocked

View File

@ -29,6 +29,7 @@
/******************************************************************************/
var stats;
var reResultParser = /^(@@)?(\*|\|\|([^$^]+)\^)\$(.+)$/;
/******************************************************************************/
@ -47,6 +48,34 @@ var formatNumber = function(count) {
/******************************************************************************/
var syncDynamicFilter = function(scope, i, result) {
var el = uDom('[data-scope="' + scope + '"] > div:nth-of-type(' + i + ')');
var matches = reResultParser.exec(result) || [];
var blocked = matches.length !== 0 && matches[1] !== '@@';
el.toggleClass('blocked', blocked);
var ownFilter = matches[3] !== undefined && matches[3] === stats.pageHostname;
el.toggleClass('ownFilter', ownFilter);
};
/******************************************************************************/
var syncAllDynamicFilters = function() {
var scopes = ['.', '/'];
var scope, results, i;
while ( scope = scopes.pop() ) {
if ( stats.dynamicFilterResults.hasOwnProperty(scope) === false ) {
continue;
}
results = stats.dynamicFilterResults[scope];
i = 5;
while ( i-- ) {
syncDynamicFilter(scope, i + 1, results[i]);
}
}
};
/******************************************************************************/
var renderStats = function() {
if ( !stats ) {
return;
@ -102,12 +131,12 @@ var renderStats = function() {
'%</span>'
);
}
uDom('#total-blocked').html(html.join(''));
uDom('#switch .fa').toggleClass(
'off',
stats.pageURL === '' || !stats.netFilteringSwitch
);
syncAllDynamicFilters();
uDom('#total-blocked').html(html.join(''));
uDom('#switch .fa').toggleClass('off', stats.pageURL === '' || !stats.netFilteringSwitch);
uDom('#dynamicFilteringToggler').toggleClass('on', stats.dynamicFilteringEnabled);
};
/******************************************************************************/
@ -185,11 +214,46 @@ var renderHeader = function() {
/******************************************************************************/
var onDynamicFilterClicked = function(ev) {
var elScope = uDom(ev.currentTarget);
var scope = elScope.attr('data-scope') === '/' ? '*' : stats.pageHostname;
var elFilter = uDom(ev.target);
var onDynamicFilterChanged = function(details) {
stats.dynamicFilterResults = details;
syncAllDynamicFilters();
};
messaging.ask({
what: 'toggleDynamicFilter',
hostname: scope,
requestType: elFilter.attr('data-type'),
firstParty: elFilter.attr('data-first-party') !== null,
block: elFilter.hasClassName('blocked') === false,
pageHostname: stats.pageHostname
}, onDynamicFilterChanged);
};
/******************************************************************************/
var toggleDynamicFiltering = function() {
var el = uDom('#dynamicFilteringToggler');
el.toggleClass('on');
messaging.tell({
what: 'userSettings',
name: 'dynamicFilteringEnabled',
value: el.hasClassName('on')
});
};
/******************************************************************************/
var installEventHandlers = function() {
uDom('h1,h2,h3,h4').on('click', gotoDashboard);
uDom('#switch .fa').on('click', toggleNetFilteringSwitch);
uDom('#gotoLog').on('click', gotoStats);
uDom('#gotoPick').on('click', gotoPick);
uDom('#dynamicFilteringToggler').on('click', toggleDynamicFiltering);
uDom('.dynamicFiltering').on('click', 'div', onDynamicFilterClicked);
};
/******************************************************************************/

View File

@ -661,13 +661,14 @@
µb.loadWhitelist(onWhitelistReady);
return;
}
µb.assets.autoUpdate = µb.userSettings.autoUpdate;
µb.loadPublicSuffixList(onPSLReady);
};
// User settings are in memory
var onUserSettingsReady = function(settings) {
µb.assets.autoUpdate = settings.autoUpdate;
µb.contextMenu.toggle(settings.contextMenuEnabled);
µb.netFilteringEngine.dynamicFiltersFromSelfie(settings.dynamicFilteringSelfie);
µb.fromSelfie(onSelfieReady);
µb.mirrors.toggle(settings.experimentalEnabled);
};

View File

@ -320,6 +320,11 @@ var onHeadersReceived = function(details) {
return;
}
// Record request
if ( result !== '' ) {
pageStore.recordResult('script', details.url, result);
}
// Blocked
pageStore.perLoadBlockedRequestCount++;
µb.localSettings.blockedRequestCount++;
@ -327,7 +332,7 @@ var onHeadersReceived = function(details) {
details.responseHeaders.push({
'name': 'Content-Security-Policy',
'value': "script-src *"
'value': "script-src 'unsafe-eval' *"
});
return { 'responseHeaders': details.responseHeaders };

View File

@ -238,7 +238,7 @@
return type;
}
var ext = path.slice(pos) + '.';
if ( '.eot.ttf.otf.svg.woff.woff2.'.indexOf(ext) !== -1 ) {
if ( '.css.eot.ttf.otf.svg.woff.woff2.'.indexOf(ext) !== -1 ) {
return 'stylesheet';
}
if ( '.ico.png.gif.jpg.jpeg.'.indexOf(ext) !== -1 ) {
@ -255,3 +255,16 @@
};
/******************************************************************************/
µBlock.toggleDynamicFilter = function(details) {
var changed = false;
if ( details.block ) {
changed = this.netFilteringEngine.dynamicFilterBlock(details.hostname, details.requestType, details.firstParty);
} else {
changed = this.netFilteringEngine.dynamicFilterUnblock(details.hostname, details.requestType, details.firstParty);
}
if ( changed ) {
this.userSettings.dynamicFilteringSelfie = this.netFilteringEngine.selfieFromDynamicFilters();
this.XAL.keyValSetOne('dynamicFilteringSelfie', this.userSettings.dynamicFilteringSelfie);
}
};

View File

@ -59,6 +59,14 @@ exports.injectScript = function(id, details) {
/******************************************************************************/
exports.keyValSetOne = function(key, val) {
var bin = {};
bin[key] = val;
chrome.storage.local.set(bin);
};
/******************************************************************************/
return exports;
/******************************************************************************/

View File

@ -24,6 +24,28 @@
<p id="total-blocked">?</p>
</div>
<div id="dynamicFilteringToggler" class="fa on"></div>
<div id="dynamicFilteringContainer">
<div class="dynamicFiltering local" data-scope=".">
<div data-first-party data-type="inline-script"></div>
<div data-first-party data-type="script"></div>
<div data-type="script"></div>
<div data-first-party data-type="sub_frame"></div>
<div data-type="sub_frame"></div>
<div class="label">&lt;script&gt;</div>
<div class="label">&lt;iframe&gt;</div>
</div>
<div class="dynamicFiltering global" data-scope="/">
<div data-first-party data-type="inline-script"></div>
<div data-first-party data-type="script"></div>
<div data-type="script"></div>
<div data-first-party data-type="sub_frame"></div>
<div data-type="sub_frame"></div>
<div class="label">&lt;script&gt;</div>
<div class="label">&lt;iframe&gt;</div>
</div>
</div>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/messaging-client.js"></script>