Add ability to quickly create exceptions in logger

This is a feature under development, hidden behind
a new advanced setting, `filterAuthorMode` which
default to `false`.

Ability to point-and-click to create temporary
exception filters for static extended filters (i.e.
cosmetic, scriptlet & html filters) from within
the summary pane in the logger. The button to
toggle on/off temporary exception filter is
labeled `#@#`.

The created exceptions are temporary and will be
lost when restarting uBO, or manually toggling off
the exception filters.

Creating temporary exception filters does not
cause the filter lists to reloaded, and thus there
is no overhead in creating/removing these temporary
exception filters.
This commit is contained in:
Raymond Hill 2019-09-24 17:05:03 -04:00
parent 733b2330de
commit 59c9a34d34
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
9 changed files with 203 additions and 47 deletions

View file

@ -660,6 +660,7 @@ body[dir="rtl"] #netFilteringDialog > .panes > .details > div > span:nth-of-type
border-left: 1px solid white;
}
#netFilteringDialog > .panes > .details > div > span:nth-of-type(2) {
flex-grow: 1;
max-height: 20vh;
overflow: hidden auto;
white-space: pre-line
@ -675,6 +676,26 @@ body[dir="rtl"] #netFilteringDialog > .panes > .details > div > span:nth-of-type
#netFilteringDialog > .panes > .details > div > span:nth-of-type(2) .fa-icon:hover {
opacity: 1;
}
#netFilteringDialog > .panes > .details .exceptor {
align-items: center;
border-left: 1px solid white;
cursor: pointer;
display: inline-flex;
font-family: monospace;
opacity: 0.8;
}
#netFilteringDialog > .panes > .details .exceptor:hover {
opacity: 1;
}
#netFilteringDialog > .panes > .details .exceptored .filter {
text-decoration: line-through;
}
#netFilteringDialog > .panes > .details .exceptored .exceptor {
background-color: lightblue;
}
#netFilteringDialog > .panes > .details .exceptor::before {
content: '#@#';
}
#netFilteringDialog > div.panes > .dynamic > .toolbar {
padding-bottom: 1em;
}

View file

@ -53,6 +53,7 @@ const µBlock = (( ) => { // jshint ignore:line
extensionUpdateForceReload: false,
ignoreRedirectFilters: false,
ignoreScriptInjectFilters: false,
filterAuthorMode: false,
loggerPopupType: 'popup',
manualUpdateAssetFetchPeriod: 500,
popupFontSize: 'unset',

View file

@ -826,6 +826,12 @@ FilterContainer.prototype.randomAlphaToken = function() {
/******************************************************************************/
FilterContainer.prototype.getSession = function() {
return this.specificFilters.session;
};
/******************************************************************************/
FilterContainer.prototype.retrieveGenericSelectors = function(request) {
if ( this.acceptedCount === 0 ) { return; }
if ( !request.ids && !request.classes ) { return; }
@ -990,12 +996,15 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
}
}
// Retrieve temporary filters
this.specificFilters.session.retrieve([ dummySet, exceptionSet ]);
// Retrieve filters with a non-empty hostname
this.specificFilters.retrieve(
hostname,
options.noSpecificCosmeticFiltering !== true
? [ specificSet, exceptionSet, proceduralSet, exceptionSet ]
: [ dummySet, exceptionSet, dummySet, exceptionSet ],
: [ dummySet, exceptionSet ],
1
);
// Retrieve filters with an empty hostname
@ -1003,7 +1012,7 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
hostname,
options.noGenericCosmeticFiltering !== true
? [ specificSet, exceptionSet, proceduralSet, exceptionSet ]
: [ dummySet, exceptionSet, dummySet, exceptionSet ],
: [ dummySet, exceptionSet ],
2
);
// Retrieve filters with a non-empty entity
@ -1012,7 +1021,7 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
`${hostname.slice(0, -request.domain.length)}${request.entity}`,
options.noSpecificCosmeticFiltering !== true
? [ specificSet, exceptionSet, proceduralSet, exceptionSet ]
: [ dummySet, exceptionSet, dummySet, exceptionSet ],
: [ dummySet, exceptionSet ],
1
);
}

View file

@ -334,6 +334,10 @@
}
};
api.getSession = function() {
return filterDB.session;
};
api.retrieve = function(details) {
const hostname = details.hostname;
@ -350,6 +354,7 @@
const procedurals = new Set();
const exceptions = new Set();
filterDB.session.retrieve([ new Set(), exceptions ]);
filterDB.retrieve(
hostname,
[ plains, exceptions, procedurals, exceptions ]

View file

@ -41,6 +41,7 @@ let filteredLoggerEntryVoidedCount = 0;
let popupLoggerBox;
let popupLoggerTooltips;
let activeTabId = 0;
let filterAuthorMode = false;
let selectedTabId = 0;
let netInspectorPaused = false;
@ -64,7 +65,7 @@ const tabIdFromAttribute = function(elem) {
// Current design allows for only one modal DOM-based dialog at any given time.
//
const modalDialog = (function() {
const modalDialog = (( ) => {
const overlay = uDom.nodeFromId('modalOverlay');
const container = overlay.querySelector(
':scope > div > div:nth-of-type(1)'
@ -949,6 +950,8 @@ const onLogBufferRead = function(response) {
allTabIdsToken = response.tabIdsToken;
}
filterAuthorMode = response.filterAuthorMode === true;
if ( activeTabIdChanged ) {
pageSelectorFromURLHash();
}
@ -1085,7 +1088,7 @@ const reloadTab = function(ev) {
/******************************************************************************/
/******************************************************************************/
(function() {
(( ) => {
const reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/;
const reSchemeOnly = /^[\w-]+:$/;
const staticFilterTypes = {
@ -1203,24 +1206,35 @@ const reloadTab = function(ev) {
);
};
const onClick = function(ev) {
const onClick = async function(ev) {
const target = ev.target;
const tcl = target.classList;
// Select a mode
if ( tcl.contains('header') ) {
dialog.setAttribute('data-pane', target.getAttribute('data-pane') );
ev.stopPropagation();
dialog.setAttribute('data-pane', target.getAttribute('data-pane') );
return;
}
// Toggle temporary exception filter
if ( tcl.contains('exceptor') ) {
ev.stopPropagation();
const status = await messaging.send('loggerUI', {
what: 'toggleTemporaryException',
filter: filterFromTargetRow(),
});
const row = target.closest('div');
row.classList.toggle('exceptored', status);
return;
}
// Create static filter
if ( target.id === 'createStaticFilter' ) {
ev.stopPropagation();
const value = staticFilterNode().value;
// Avoid duplicates
if ( createdStaticFilters.hasOwnProperty(value) ) {
return;
}
if ( createdStaticFilters.hasOwnProperty(value) ) { return; }
createdStaticFilters[value] = true;
if ( value !== '' ) {
messaging.send('loggerUI', {
@ -1232,21 +1246,19 @@ const reloadTab = function(ev) {
});
}
updateWidgets();
ev.stopPropagation();
return;
}
// Save url filtering rule(s)
if ( target.id === 'saveRules' ) {
messaging.send('loggerUI', {
ev.stopPropagation();
await messaging.send('loggerUI', {
what: 'saveURLFilteringRules',
context: selectValue('select.dynamic.origin'),
urls: targetURLs,
type: uglyTypeFromSelector('dynamic'),
}).then(( ) => {
colorize();
});
ev.stopPropagation();
colorize();
return;
}
@ -1254,87 +1266,83 @@ const reloadTab = function(ev) {
// Remove url filtering rule
if ( tcl.contains('action') ) {
messaging.send('loggerUI', {
ev.stopPropagation();
await messaging.send('loggerUI', {
what: 'setURLFilteringRule',
context: selectValue('select.dynamic.origin'),
url: target.getAttribute('data-url'),
type: uglyTypeFromSelector('dynamic'),
action: 0,
persist: persist,
}).then(( ) => {
colorize();
});
ev.stopPropagation();
colorize();
return;
}
// add "allow" url filtering rule
if ( tcl.contains('allow') ) {
messaging.send('loggerUI', {
ev.stopPropagation();
await messaging.send('loggerUI', {
what: 'setURLFilteringRule',
context: selectValue('select.dynamic.origin'),
url: target.parentNode.getAttribute('data-url'),
type: uglyTypeFromSelector('dynamic'),
action: 2,
persist: persist,
}).then(( ) => {
colorize();
});
ev.stopPropagation();
colorize();
return;
}
// add "block" url filtering rule
if ( tcl.contains('noop') ) {
messaging.send('loggerUI', {
ev.stopPropagation();
await messaging.send('loggerUI', {
what: 'setURLFilteringRule',
context: selectValue('select.dynamic.origin'),
url: target.parentNode.getAttribute('data-url'),
type: uglyTypeFromSelector('dynamic'),
action: 3,
persist: persist,
}).then(( ) => {
colorize();
});
ev.stopPropagation();
colorize();
return;
}
// add "block" url filtering rule
if ( tcl.contains('block') ) {
messaging.send('loggerUI', {
ev.stopPropagation();
await messaging.send('loggerUI', {
what: 'setURLFilteringRule',
context: selectValue('select.dynamic.origin'),
url: target.parentNode.getAttribute('data-url'),
type: uglyTypeFromSelector('dynamic'),
action: 1,
persist: persist,
}).then(( ) => {
colorize();
});
ev.stopPropagation();
colorize();
return;
}
// Force a reload of the tab
if ( tcl.contains('reload') ) {
ev.stopPropagation();
messaging.send('loggerUI', {
what: 'reloadTab',
tabId: targetTabId,
});
ev.stopPropagation();
return;
}
// Hightlight corresponding element in target web page
if ( tcl.contains('picker') ) {
ev.stopPropagation();
messaging.send('loggerUI', {
what: 'launchElementPicker',
tabId: targetTabId,
targetURL: 'img\t' + targetURLs[0],
select: true,
});
ev.stopPropagation();
return;
}
};
@ -1426,6 +1434,37 @@ const reloadTab = function(ev) {
return urls;
};
const filterFromTargetRow = function() {
return targetRow.children[1].textContent;
};
const toSummaryPaneFilterNode = async function(receiver, filter) {
receiver.children[1].textContent = filter;
if ( filterAuthorMode !== true ) { return; }
const match = /#@?#/.exec(filter);
if ( match === null ) { return; }
const fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode(match[0]));
const selector = filter.slice(match.index + match[0].length);
const span = document.createElement('span');
span.className = 'filter';
span.textContent = selector;
fragment.appendChild(span);
let isTemporaryException = false;
if ( match[0] === '#@#' ) {
isTemporaryException = await messaging.send('loggerUI', {
what: 'hasTemporaryException',
filter,
});
receiver.classList.toggle('exceptored', isTemporaryException);
}
if ( match[0] === '##' || isTemporaryException ) {
receiver.children[2].style.visibility = '';
}
receiver.children[1].textContent = '';
receiver.children[1].appendChild(fragment);
};
const fillSummaryPaneFilterList = async function(rows) {
const rawFilter = targetRow.children[1].textContent;
const compiledFilter = targetRow.getAttribute('data-filter');
@ -1468,7 +1507,7 @@ const reloadTab = function(ev) {
bestMatchFilter !== '' &&
Array.isArray(response[bestMatchFilter])
) {
rows[0].children[1].textContent = bestMatchFilter;
toSummaryPaneFilterNode(rows[0], bestMatchFilter);
rows[1].children[1].appendChild(nodeFromFilter(
bestMatchFilter,
response[bestMatchFilter]
@ -1499,7 +1538,7 @@ const reloadTab = function(ev) {
});
handleResponse(response);
}
};
} ;
const fillSummaryPane = function() {
const rows = dialog.querySelectorAll('.pane.details > div');
@ -1508,12 +1547,12 @@ const reloadTab = function(ev) {
const trch = tr.children;
let text;
// Filter and context
text = trch[1].textContent;
text = filterFromTargetRow();
if (
(text !== '') &&
(trcl.contains('cosmeticRealm') || trcl.contains('networkRealm'))
) {
rows[0].children[1].textContent = text;
toSummaryPaneFilterNode(rows[0], text);
} else {
rows[0].style.display = 'none';
}
@ -1753,7 +1792,7 @@ const reloadTab = function(ev) {
fillSummaryPane();
fillDynamicPane();
fillStaticPane();
dialog.addEventListener('click', onClick, true);
dialog.addEventListener('click', ev => { onClick(ev); }, true);
dialog.addEventListener('change', onSelectChange, true);
dialog.addEventListener('input', onInputChange, true);
modalDialog.show();

View file

@ -1203,10 +1203,11 @@ const extensionOriginURL = vAPI.getURL('');
const getLoggerData = async function(details, activeTabId, callback) {
const response = {
activeTabId,
colorBlind: µb.userSettings.colorBlindFriendly,
entries: µb.logger.readAll(details.ownerId),
filterAuthorMode: µb.hiddenSettings.filterAuthorMode,
maxEntries: µb.userSettings.requestLogMaxEntries,
activeTabId: activeTabId,
tabIdsToken: µb.pageStoresToken,
tooltips: µb.userSettings.tooltipsDisabled === false
};
@ -1278,6 +1279,41 @@ const getURLFilteringData = function(details) {
return response;
};
const compileTemporaryException = function(filter) {
const match = /#@?#/.exec(filter);
if ( match === null ) { return; }
let selector = filter.slice(match.index + match[0].length);
let session;
if ( selector.startsWith('+js') ) {
session = µb.scriptletFilteringEngine.getSession();
selector = selector.slice(4, -1).trim();
} else {
if ( selector.startsWith('^') ) {
session = µb.htmlFilteringEngine.getSession();
selector = selector.slice(1).trim();
} else {
session = µb.cosmeticFilteringEngine.getSession();
}
selector = µb.staticExtFilteringEngine.compileSelector(selector);
}
return { session, selector };
};
const toggleTemporaryException = function(details) {
const { session, selector } = compileTemporaryException(details.filter);
if ( session.has(1, selector) ) {
session.remove(1, selector);
return false;
}
session.add(1, selector);
return true;
};
const hasTemporaryException = function(details) {
const { session, selector } = compileTemporaryException(details.filter);
return session && session.has(1, selector);
};
const onMessage = function(request, sender, callback) {
// Async
switch ( request.what ) {
@ -1301,6 +1337,10 @@ const onMessage = function(request, sender, callback) {
let response;
switch ( request.what ) {
case 'hasTemporaryException':
response = hasTemporaryException(request);
break;
case 'releaseView':
if ( request.ownerId === µb.logger.ownerId ) {
µb.logger.ownerId = undefined;
@ -1327,6 +1367,10 @@ const onMessage = function(request, sender, callback) {
response = getURLFilteringData(request);
break;
case 'toggleTemporaryException':
response = toggleTemporaryException(request);
break;
default:
return vAPI.messaging.UNHANDLED;
}

View file

@ -342,6 +342,10 @@
}
};
api.getSession = function() {
return scriptletDB.session;
};
const scriptlets$ = new Set();
const exceptions$ = new Set();
const scriptletToCodeMap$ = new Map();
@ -367,10 +371,8 @@
scriptlets$.clear();
exceptions$.clear();
scriptletDB.retrieve(
hostname,
[ scriptlets$, exceptions$ ]
);
scriptletDB.session.retrieve([ scriptlets$, exceptions$ ]);
scriptletDB.retrieve(hostname, [ scriptlets$, exceptions$ ]);
if ( request.entity !== '' ) {
scriptletDB.retrieve(
`${hostname.slice(0, -request.domain.length)}${request.entity}`,

View file

@ -50,7 +50,7 @@
**/
µBlock.staticExtFilteringEngine = (function() {
µBlock.staticExtFilteringEngine = (( ) => {
const µb = µBlock;
const reHasUnicode = /[^\x00-\x7F]/;
const reParseRegexLiteral = /^\/(.+)\/([imu]+)?$/;
@ -520,10 +520,45 @@
// Avoid heterogeneous arrays. Thus:
this.hostnameSlots = []; // array of integers
// IMPORTANT: initialize with an empty array because -0 is NOT < 0.
this.hostnameSlotsEx = [ [] ]; // Array of arrays of integers
this.hostnameSlotsEx = [ [] ]; // array of arrays of integers
// Array of strings (selectors and pseudo-selectors)
this.strSlots = [];
this.size = 0;
// Temporary set
this.session = {
collection: new Map(),
add: function(bits, s) {
const bucket = this.collection.get(bits);
if ( bucket === undefined ) {
this.collection.set(bits, new Set([ s ]));
} else {
bucket.add(s);
}
},
remove: function(bits, s) {
const bucket = this.collection.get(bits);
if ( bucket === undefined ) { return; }
bucket.delete(s);
if ( bucket.size !== 0 ) { return; }
this.collection.delete(bits);
},
retrieve(out) {
const mask = out.length - 1;
for ( const [ bits, bucket ] of this.collection ) {
for ( const s of bucket ) {
out[bits & mask].add(s);
}
}
},
has(bits, s) {
const selectors = this.collection.get(bits);
return selectors !== undefined && selectors.has(s);
},
clear() {
this.collection.clear();
},
};
if ( selfie !== undefined ) {
this.fromSelfie(selfie);
}
@ -673,7 +708,7 @@
// https://github.com/uBlockOrigin/uBlock-issues/issues/89
// Do not discard unknown pseudo-elements.
api.compileSelector = (function() {
api.compileSelector = (( ) => {
const reAfterBeforeSelector = /^(.+?)(::?after|::?before|::[a-z-]+)$/;
const reStyleSelector = /^(.+?):style\((.+?)\)$/;
const reExtendedSyntax = /\[-(?:abp|ext)-[a-z-]+=(['"])(?:.+?)(?:\1)\]/;

View file

@ -112,7 +112,7 @@
</div>
<div class="panes">
<div class="pane details" data-pane="details">
<div><span data-i18n="loggerEntryDetailsFilter"></span><span></span></div>
<div><span data-i18n="loggerEntryDetailsFilter"></span><span></span><span class="exceptor" style="visibility: collapse"></span></div>
<div><span data-i18n="loggerEntryDetailsFilterList"></span><span class="prose"></span></div>
<div><span data-i18n="loggerEntryDetailsRule"></span><span></span></div>
<div><span data-i18n="loggerEntryDetailsRootContext"></span><span></span></div>