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 @@
-
+