Skip to content

Commit

Permalink
Add ability to quickly create exceptions in logger
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gorhill committed Sep 24, 2019
1 parent 733b233 commit 59c9a34
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 47 deletions.
21 changes: 21 additions & 0 deletions src/css/logger-ui.css
Expand Up @@ -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
Expand All @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/js/background.js
Expand Up @@ -53,6 +53,7 @@ const µBlock = (( ) => { // jshint ignore:line
extensionUpdateForceReload: false,
ignoreRedirectFilters: false,
ignoreScriptInjectFilters: false,
filterAuthorMode: false,
loggerPopupType: 'popup',
manualUpdateAssetFetchPeriod: 500,
popupFontSize: 'unset',
Expand Down
15 changes: 12 additions & 3 deletions src/js/cosmetic-filtering.js
Expand Up @@ -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; }
Expand Down Expand Up @@ -990,20 +996,23 @@ 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
this.specificFilters.retrieve(
hostname,
options.noGenericCosmeticFiltering !== true
? [ specificSet, exceptionSet, proceduralSet, exceptionSet ]
: [ dummySet, exceptionSet, dummySet, exceptionSet ],
: [ dummySet, exceptionSet ],
2
);
// Retrieve filters with a non-empty entity
Expand All @@ -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
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/js/html-filtering.js
Expand Up @@ -334,6 +334,10 @@
}
};

api.getSession = function() {
return filterDB.session;
};

api.retrieve = function(details) {
const hostname = details.hostname;

Expand All @@ -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 ]
Expand Down
109 changes: 74 additions & 35 deletions src/js/logger-ui.js
Expand Up @@ -41,6 +41,7 @@ let filteredLoggerEntryVoidedCount = 0;
let popupLoggerBox;
let popupLoggerTooltips;
let activeTabId = 0;
let filterAuthorMode = false;
let selectedTabId = 0;
let netInspectorPaused = false;

Expand All @@ -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)'
Expand Down Expand Up @@ -949,6 +950,8 @@ const onLogBufferRead = function(response) {
allTabIdsToken = response.tabIdsToken;
}

filterAuthorMode = response.filterAuthorMode === true;

if ( activeTabIdChanged ) {
pageSelectorFromURLHash();
}
Expand Down Expand Up @@ -1085,7 +1088,7 @@ const reloadTab = function(ev) {
/******************************************************************************/
/******************************************************************************/

(function() {
(( ) => {
const reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/;
const reSchemeOnly = /^[\w-]+:$/;
const staticFilterTypes = {
Expand Down Expand Up @@ -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', {
Expand All @@ -1232,109 +1246,103 @@ 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;
}

const persist = !!ev.ctrlKey || !!ev.metaKey;

// 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;
}
};
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -1499,7 +1538,7 @@ const reloadTab = function(ev) {
});
handleResponse(response);
}
};
} ;

const fillSummaryPane = function() {
const rows = dialog.querySelectorAll('.pane.details > div');
Expand All @@ -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';
}
Expand Down Expand Up @@ -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();
Expand Down

12 comments on commit 59c9a34

@uBlock-user
Copy link
Contributor

@uBlock-user uBlock-user commented on 59c9a34 Sep 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why make it temporary though ?

Edit: I suggest you add a lock icon there like we have with dynamic filtering, so those who want to make it permanent, can do it from the logger itself.

@uBlock-user
Copy link
Contributor

@uBlock-user uBlock-user commented on 59c9a34 Sep 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I set filterAuthorMode to true, then hostname is not shown in the specific cosmetic filters. Why is it like that ?

image

image

If I set to false which is the default, it works as expected. Another regression ?

Test link -- https://www.outtherecolorado.com/colorado-woman-chases-thrill-of-hiking-active-volcanoes/

@gwarser
Copy link
Contributor

@gwarser gwarser commented on 59c9a34 Sep 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#@# button highlighting is lost when dialog is closed and reopened.


Fixed in 1.22.5b0

@gorhill
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Ok, reading again I agree I read too much into the question, sorry about this -- deleted).

I couldn't remember where this was informally discussed, I would have added a link to this in the commit.

The reason it's temporary is so that uBO does not have to reload all the filter lists -- it's something I have mentioned in a few places I wanted to implement since a while, a quick way to toggle temporary exception filters for quick test purpose without having to suffer the memory churn of reloading all the filter lists. The next step is to provide the same functionality for static network filters.

@uBlock-user
Copy link
Contributor

@uBlock-user uBlock-user commented on 59c9a34 Sep 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a quick way to toggle temporary exception filters for quick test purpose without having to suffer the memory churn of reloading all the filter lists.

In that sentiment, you could disable reverse-lookup for filterlist for these temp. filters as they're not gonna be found in any filterlist anyways. Currently, it will show Static filter [insert filter here] could not be found in any of the currently enabled filter lists, so why not just disable it as the reverse lookup will never find the filter. Just an idea.

@gorhill
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to keep reverse-lookup as is, it may be useful to check whether the exception has been added to My filters or any one of the enabled lists -- people can still force a list update of add custom filters after a temporary exception has been created.

@uBlock-user
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are temp. scriptlet exception filters shown/created as generic filters ?

image

image

@gorhill
Copy link
Owner Author

@gorhill gorhill commented on 59c9a34 Oct 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because they are generic internally.

@uBlock-user
Copy link
Contributor

@uBlock-user uBlock-user commented on 59c9a34 Oct 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That means a filter which is disabled on one site will be disabled everywhere even though that's not the intention for the filter author when testing -- example.com,google.com##+js(set, google, noopfunc)
Disabling the above filter on example.com will disable on google.com too.

@gorhill
Copy link
Owner Author

@gorhill gorhill commented on 59c9a34 Oct 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

These temporary exceptions are meant to quickly disable a filter for testing purpose. It's not yet in there as I wanted to think more about it but I will quite probably have all the temporary filters removed when the logger is closed.

@uBlock-user
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume these temp filters are removed if I close the browser ?

@gorhill
Copy link
Owner Author

@gorhill gorhill commented on 59c9a34 Oct 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are not persisted, so they go away when uBO is reloaded.

Please sign in to comment.