From 59c9a34d34a737f6bb48c4130c65f4fe0fa73806 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Tue, 24 Sep 2019 17:05:03 -0400 Subject: [PATCH] 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. --- src/css/logger-ui.css | 21 +++++++ src/js/background.js | 1 + src/js/cosmetic-filtering.js | 15 ++++- src/js/html-filtering.js | 5 ++ src/js/logger-ui.js | 109 ++++++++++++++++++++++----------- src/js/messaging.js | 46 +++++++++++++- src/js/scriptlet-filtering.js | 10 +-- src/js/static-ext-filtering.js | 41 ++++++++++++- src/logger-ui.html | 2 +- 9 files changed, 203 insertions(+), 47 deletions(-) diff --git a/src/css/logger-ui.css b/src/css/logger-ui.css index cb3316b04..3df70a861 100644 --- a/src/css/logger-ui.css +++ b/src/css/logger-ui.css @@ -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; } diff --git a/src/js/background.js b/src/js/background.js index 15172ad95..5b6a3e846 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -53,6 +53,7 @@ const µBlock = (( ) => { // jshint ignore:line extensionUpdateForceReload: false, ignoreRedirectFilters: false, ignoreScriptInjectFilters: false, + filterAuthorMode: false, loggerPopupType: 'popup', manualUpdateAssetFetchPeriod: 500, popupFontSize: 'unset', diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index 6506daf20..d8534c49b 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -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 ); } diff --git a/src/js/html-filtering.js b/src/js/html-filtering.js index d1118ba46..9d7c8c19d 100644 --- a/src/js/html-filtering.js +++ b/src/js/html-filtering.js @@ -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 ] diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index 918bbda5a..76e3f2db4 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -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(); diff --git a/src/js/messaging.js b/src/js/messaging.js index 6ff0fc21f..9ddcd968d 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -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; } diff --git a/src/js/scriptlet-filtering.js b/src/js/scriptlet-filtering.js index 1424076e4..e83c44e90 100644 --- a/src/js/scriptlet-filtering.js +++ b/src/js/scriptlet-filtering.js @@ -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}`, diff --git a/src/js/static-ext-filtering.js b/src/js/static-ext-filtering.js index 52fa4ad9b..53da1919d 100644 --- a/src/js/static-ext-filtering.js +++ b/src/js/static-ext-filtering.js @@ -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)\]/; diff --git a/src/logger-ui.html b/src/logger-ui.html index da31228bb..0faad764b 100644 --- a/src/logger-ui.html +++ b/src/logger-ui.html @@ -112,7 +112,7 @@
-
+