diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 05:50:18 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 05:50:18 +0000 |
commit | 55a5d29a66503248916f249ad2a1d8b37cde5a03 (patch) | |
tree | a3beb6a90dd3bdaaf67ecb05d42152a494aff946 /src/js | |
parent | Adding upstream version 1.55.0+dfsg. (diff) | |
download | ublock-origin-59fb3c3fd76fcdb30d9acb3ea43d538729eedc25.tar.xz ublock-origin-59fb3c3fd76fcdb30d9acb3ea43d538729eedc25.zip |
Adding upstream version 1.57.0+dfsg.upstream/1.57.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/js')
52 files changed, 3986 insertions, 1970 deletions
diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js index fc50b50..70ce256 100644 --- a/src/js/1p-filters.js +++ b/src/js/1p-filters.js @@ -21,12 +21,10 @@ /* global CodeMirror, uBlockDashboard */ -'use strict'; - -import { onBroadcast } from './broadcast.js'; +import './codemirror/ubo-static-filtering.js'; import { dom, qs$ } from './dom.js'; import { i18n$ } from './i18n.js'; -import './codemirror/ubo-static-filtering.js'; +import { onBroadcast } from './broadcast.js'; /******************************************************************************/ @@ -53,8 +51,6 @@ const cmEditor = new CodeMirror(qs$('#userFilters'), { uBlockDashboard.patchCodeMirrorEditor(cmEditor); -let cachedUserFilters = ''; - /******************************************************************************/ // Add auto-complete ability to the editor. Polling is used as the suggested @@ -91,9 +87,32 @@ vAPI.messaging.send('dashboard', { /******************************************************************************/ +let originalState = { + enabled: true, + trusted: false, + filters: '', +}; + +function getCurrentState() { + const enabled = qs$('#enableMyFilters input').checked; + return { + enabled, + trusted: enabled && qs$('#trustMyFilters input').checked, + filters: getEditorText(), + }; +} + +function rememberCurrentState() { + originalState = getCurrentState(); +} + +function currentStateChanged() { + return JSON.stringify(getCurrentState()) !== JSON.stringify(originalState); +} + function getEditorText() { const text = cmEditor.getValue().replace(/\s+$/, ''); - return text === '' ? text : text + '\n'; + return text === '' ? text : `${text}\n`; } function setEditorText(text) { @@ -102,12 +121,30 @@ function setEditorText(text) { /******************************************************************************/ -function userFiltersChanged(changed) { - if ( typeof changed !== 'boolean' ) { - changed = self.hasUnsavedData(); - } +function userFiltersChanged(details = {}) { + const changed = typeof details.changed === 'boolean' + ? details.changed + : self.hasUnsavedData(); qs$('#userFiltersApply').disabled = !changed; qs$('#userFiltersRevert').disabled = !changed; + const enabled = qs$('#enableMyFilters input').checked; + dom.attr('#trustMyFilters .input.checkbox', 'disabled', enabled ? null : ''); + const trustedbefore = cmEditor.getOption('trustedSource'); + const trustedAfter = enabled && qs$('#trustMyFilters input').checked; + if ( trustedAfter === trustedbefore ) { return; } + cmEditor.startOperation(); + cmEditor.setOption('trustedSource', trustedAfter); + const doc = cmEditor.getDoc(); + const history = doc.getHistory(); + const selections = doc.listSelections(); + doc.replaceRange(doc.getValue(), + { line: 0, ch: 0 }, + { line: doc.lineCount(), ch: 0 } + ); + doc.setSelections(selections); + doc.setHistory(history); + cmEditor.endOperation(); + cmEditor.focus(); } /******************************************************************************/ @@ -118,7 +155,7 @@ function userFiltersChanged(changed) { // background. function threeWayMerge(newContent) { - const prvContent = cachedUserFilters.trim().split(/\n/); + const prvContent = originalState.filters.trim().split(/\n/); const differ = new self.diff_match_patch(); const newChanges = differ.diff( prvContent, @@ -167,19 +204,22 @@ async function renderUserFilters(merge = false) { }); if ( details instanceof Object === false || details.error ) { return; } - cmEditor.setOption('trustedSource', details.trustedSource === true); + cmEditor.setOption('trustedSource', details.trusted); + + qs$('#enableMyFilters input').checked = details.enabled; + qs$('#trustMyFilters input').checked = details.trusted; const newContent = details.content.trim(); if ( merge && self.hasUnsavedData() ) { setEditorText(threeWayMerge(newContent)); - userFiltersChanged(true); + userFiltersChanged({ changed: true }); } else { setEditorText(newContent); - userFiltersChanged(false); + userFiltersChanged({ changed: false }); } - cachedUserFilters = newContent; + rememberCurrentState(); } /******************************************************************************/ @@ -224,7 +264,7 @@ function exportUserFiltersToFile() { .replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString()) .replace(/ +/g, '_'); vAPI.download({ - 'url': 'data:text/plain;charset=utf-8,' + encodeURIComponent(val + '\n'), + 'url': `data:text/plain;charset=utf-8,${encodeURIComponent(val)}`, 'filename': filename }); } @@ -232,21 +272,26 @@ function exportUserFiltersToFile() { /******************************************************************************/ async function applyChanges() { + const state = getCurrentState(); const details = await vAPI.messaging.send('dashboard', { what: 'writeUserFilters', - content: getEditorText(), + content: state.filters, + enabled: state.enabled, + trusted: state.trusted, }); if ( details instanceof Object === false || details.error ) { return; } - - cachedUserFilters = details.content.trim(); - userFiltersChanged(false); + rememberCurrentState(); + userFiltersChanged({ changed: false }); vAPI.messaging.send('dashboard', { what: 'reloadAllFilters', }); } function revertChanges() { - setEditorText(cachedUserFilters); + qs$('#enableMyFilters input').checked = originalState.enabled; + qs$('#trustMyFilters input').checked = originalState.trusted; + setEditorText(originalState.filters); + userFiltersChanged(); } /******************************************************************************/ @@ -268,8 +313,10 @@ self.cloud.onPull = setCloudData; /******************************************************************************/ +self.wikilink = 'https://github.com/gorhill/uBlock/wiki/Dashboard:-My-filters'; + self.hasUnsavedData = function() { - return getEditorText().trim() !== cachedUserFilters; + return currentStateChanged(); }; /******************************************************************************/ @@ -278,6 +325,8 @@ self.hasUnsavedData = function() { dom.on('#exportUserFiltersToFile', 'click', exportUserFiltersToFile); dom.on('#userFiltersApply', 'click', ( ) => { applyChanges(); }); dom.on('#userFiltersRevert', 'click', revertChanges); +dom.on('#enableMyFilters input', 'change', userFiltersChanged); +dom.on('#trustMyFilters input', 'change', userFiltersChanged); (async ( ) => { await renderUserFilters(); diff --git a/src/js/3p-filters.js b/src/js/3p-filters.js index c59365f..2d1a5df 100644 --- a/src/js/3p-filters.js +++ b/src/js/3p-filters.js @@ -19,11 +19,9 @@ Home: https://github.com/gorhill/uBlock */ -'use strict'; - -import { onBroadcast } from './broadcast.js'; import { dom, qs$, qsa$ } from './dom.js'; import { i18n, i18n$ } from './i18n.js'; +import { onBroadcast } from './broadcast.js'; /******************************************************************************/ @@ -32,6 +30,10 @@ const obsoleteTemplateString = i18n$('3pExternalListObsolete'); const reValidExternalList = /^[a-z-]+:\/\/(?:\S+\/\S*|\/\S+)/m; const recentlyUpdated = 1 * 60 * 60 * 1000; // 1 hour +// https://eslint.org/docs/latest/rules/no-prototype-builtins +const hasOwnProperty = (o, p) => + Object.prototype.hasOwnProperty.call(o, p); + let listsetDetails = {}; /******************************************************************************/ @@ -74,7 +76,9 @@ const renderNodeStats = (used, total) => { }; const i18nGroupName = name => { - return i18n$('3pGroup' + name.charAt(0).toUpperCase() + name.slice(1)); + const groupname = i18n$('3pGroup' + name.charAt(0).toUpperCase() + name.slice(1)); + if ( groupname !== '' ) { return groupname; } + return `${name.charAt(0).toLocaleUpperCase}${name.slice(1)}`; }; /******************************************************************************/ @@ -90,8 +94,9 @@ const renderFilterLists = ( ) => { const initializeListEntry = (listDetails, listEntry) => { const listkey = listEntry.dataset.key; + const groupkey = listDetails.group2 || listDetails.group; const listEntryPrevious = - qs$(`[data-key="${listDetails.group}"] [data-key="${listkey}"]`); + qs$(`[data-key="${groupkey}"] [data-key="${listkey}"]`); if ( listEntryPrevious !== null ) { if ( dom.cl.has(listEntryPrevious, 'checked') ) { dom.cl.add(listEntry, 'checked'); @@ -179,6 +184,9 @@ const renderFilterLists = ( ) => { if ( depth !== 0 ) { const reEmojis = /\p{Emoji}+/gu; treeEntries.sort((a ,b) => { + const ap = a[1].preferred === true; + const bp = b[1].preferred === true; + if ( ap !== bp ) { return ap ? -1 : 1; } const as = (a[1].title || a[0]).replace(reEmojis, ''); const bs = (b[1].title || b[0]).replace(reEmojis, ''); return as.localeCompare(bs); @@ -223,8 +231,11 @@ const renderFilterLists = ( ) => { 'privacy', 'malware', 'multipurpose', + 'cookies', + 'social', 'annoyances', 'regions', + 'unknown', 'custom' ]; for ( const key of groupKeys ) { @@ -234,17 +245,20 @@ const renderFilterLists = ( ) => { }; } for ( const [ listkey, listDetails ] of Object.entries(response.available) ) { - let groupKey = listDetails.group; - if ( groupKey === 'social' ) { - groupKey = 'annoyances'; + let groupkey = listDetails.group2 || listDetails.group; + if ( hasOwnProperty(listTree, groupkey) === false ) { + groupkey = 'unknown'; } - const groupDetails = listTree[groupKey]; + const groupDetails = listTree[groupkey]; if ( listDetails.parent !== undefined ) { let lists = groupDetails.lists; for ( const parent of listDetails.parent.split('|') ) { if ( lists[parent] === undefined ) { lists[parent] = { title: parent, lists: {} }; } + if ( listDetails.preferred === true ) { + lists[parent].preferred = true; + } lists = lists[parent].lists; } lists[listkey] = listDetails; @@ -253,6 +267,15 @@ const renderFilterLists = ( ) => { groupDetails.lists[listkey] = listDetails; } } + // https://github.com/uBlockOrigin/uBlock-issues/issues/3154#issuecomment-1975413427 + // Remove empty sections + for ( const groupkey of groupKeys ) { + const groupDetails = listTree[groupkey]; + if ( groupDetails === undefined ) { continue; } + if ( Object.keys(groupDetails.lists).length !== 0 ) { continue; } + delete listTree[groupkey]; + } + const listEntries = createListEntries('root', listTree); qs$('#lists .listEntries').replaceWith(listEntries); @@ -286,14 +309,14 @@ const renderFilterLists = ( ) => { /******************************************************************************/ const renderWidgets = ( ) => { + const updating = dom.cl.has(dom.body, 'updating'); + const hasObsolete = qs$('#lists .listEntry.checked.obsolete:not(.toRemove)') !== null; dom.cl.toggle('#buttonApply', 'disabled', filteringSettingsHash === hashFromCurrentFromSettings() ); - const updating = dom.cl.has(dom.body, 'updating'); dom.cl.toggle('#buttonUpdate', 'active', updating); dom.cl.toggle('#buttonUpdate', 'disabled', - updating === false && - qs$('#lists .listEntry.checked.obsolete:not(.toRemove)') === null + updating === false && hasObsolete === false ); }; @@ -530,6 +553,35 @@ dom.on('#lists', 'click', 'span.cache', onPurgeClicked); /******************************************************************************/ const selectFilterLists = async ( ) => { + // External filter lists to import + // Find stock list matching entries in lists to import + const toImport = (( ) => { + const textarea = qs$('#lists .listEntry[data-role="import"].expanded textarea'); + if ( textarea === null ) { return ''; } + const lists = listsetDetails.available; + const lines = textarea.value.split(/\s+\n|\s+/); + const after = []; + for ( const line of lines ) { + if ( /^https?:\/\//.test(line) === false ) { continue; } + for ( const [ listkey, list ] of Object.entries(lists) ) { + if ( list.content !== 'filters' ) { continue; } + if ( list.contentURL === undefined ) { continue; } + if ( list.contentURL.includes(line) === false ) { + after.push(line); + continue; + } + const groupkey = list.group2 || list.group; + const listEntry = qs$(`[data-key="${groupkey}"] [data-key="${listkey}"]`); + if ( listEntry === null ) { break; } + toggleFilterList(listEntry, true); + break; + } + } + dom.cl.remove(textarea.closest('.expandable'), 'expanded'); + textarea.value = ''; + return after.join('\n'); + })(); + // Cosmetic filtering switch let checked = qs$('#parseCosmeticFilters').checked; vAPI.messaging.send('dashboard', { @@ -552,7 +604,7 @@ const selectFilterLists = async ( ) => { const toRemove = []; for ( const liEntry of qsa$('#lists .listEntry[data-role="leaf"]') ) { const listkey = liEntry.dataset.key; - if ( listsetDetails.available.hasOwnProperty(listkey) === false ) { + if ( hasOwnProperty(listsetDetails.available, listkey) === false ) { continue; } const listDetails = listsetDetails.available[listkey]; @@ -569,14 +621,6 @@ const selectFilterLists = async ( ) => { } } - // External filter lists to import - const textarea = qs$('#lists .listEntry[data-role="import"].expanded textarea'); - const toImport = textarea !== null && textarea.value.trim() || ''; - if ( textarea !== null ) { - dom.cl.remove(textarea.closest('.expandable'), 'expanded'); - textarea.value = ''; - } - hashFromListsetDetails(); await vAPI.messaging.send('dashboard', { @@ -630,7 +674,7 @@ dom.on('#suspendUntilListsAreLoaded', 'change', userSettingCheckboxChanged); /******************************************************************************/ const searchFilterLists = ( ) => { - const pattern = dom.prop('.searchbar input', 'value') || ''; + const pattern = dom.prop('.searchfield input', 'value') || ''; dom.cl.toggle('#lists', 'searchMode', pattern !== ''); if ( pattern === '' ) { return; } const reflectSearchMatches = listEntry => { @@ -657,10 +701,11 @@ const searchFilterLists = ( ) => { if ( listDetails === undefined ) { continue; } let haystack = perListHaystack.get(listDetails); if ( haystack === undefined ) { + const groupkey = listDetails.group2 || listDetails.group || ''; haystack = [ listDetails.title, - listDetails.group || '', - i18nGroupName(listDetails.group || ''), + groupkey, + i18nGroupName(groupkey), listDetails.tags || '', toI18n(listDetails.tags || ''), ].join(' ').trim(); @@ -673,14 +718,13 @@ const searchFilterLists = ( ) => { const perListHaystack = new WeakMap(); -dom.on('.searchbar input', 'input', searchFilterLists); +dom.on('.searchfield input', 'input', searchFilterLists); /******************************************************************************/ const expandedListSet = new Set([ - 'uBlock filters', - 'AdGuard – Annoyances', - 'EasyList – Annoyances', + 'cookies', + 'social', ]); const listIsExpanded = which => { @@ -844,6 +888,8 @@ self.cloud.onPull = function fromCloudData(data, append) { /******************************************************************************/ +self.wikilink = 'https://github.com/gorhill/uBlock/wiki/Dashboard:-Filter-lists'; + self.hasUnsavedData = function() { return hashFromCurrentFromSettings() !== filteringSettingsHash; }; diff --git a/src/js/asset-viewer.js b/src/js/asset-viewer.js index eabe136..351531b 100644 --- a/src/js/asset-viewer.js +++ b/src/js/asset-viewer.js @@ -60,6 +60,7 @@ import './codemirror/ubo-static-filtering.js'; lineWrapping: true, matchBrackets: true, maxScanLines: 1, + maximizable: false, readOnly: true, styleActiveLine: { nonEmpty: true, diff --git a/src/js/assets.js b/src/js/assets.js index 69c2ef3..e1bc4e6 100644 --- a/src/js/assets.js +++ b/src/js/assets.js @@ -53,10 +53,13 @@ let remoteServerFriendly = false; const stringIsNotEmpty = s => typeof s === 'string' && s !== ''; const parseExpires = s => { - const matches = s.match(/(\d+)\s*([dhm]?)/i); + const matches = s.match(/(\d+)\s*([wdhm]?)/i); if ( matches === null ) { return; } let updateAfter = parseInt(matches[1], 10); - if ( matches[2] === 'h' ) { + if ( updateAfter === 0 ) { return; } + if ( matches[2] === 'w' ) { + updateAfter *= 7 * 24; + } else if ( matches[2] === 'h' ) { updateAfter = Math.max(updateAfter, 4) / 24; } else if ( matches[2] === 'm' ) { updateAfter = Math.max(updateAfter, 240) / 1440; @@ -428,7 +431,7 @@ assets.fetchFilterList = async function(mainlistURL) { continue; } if ( result instanceof Object === false ) { continue; } - const content = result.content; + const content = result.content.trimEnd() + '\n'; const slices = sfp.utils.preparser.splitter( content, vAPI.webextFlavor.env @@ -500,7 +503,7 @@ assets.fetchFilterList = async function(mainlistURL) { resourceTime, content: allParts.length === 1 ? allParts[0] - : allParts.join('') + '\n' + : allParts.join('') }; }; @@ -525,12 +528,12 @@ function getAssetSourceRegistry() { assetSourceRegistryPromise = cacheStorage.get( 'assetSourceRegistry' ).then(bin => { - if ( - bin instanceof Object && - bin.assetSourceRegistry instanceof Object - ) { - assetSourceRegistry = bin.assetSourceRegistry; - return assetSourceRegistry; + if ( bin instanceof Object ) { + if ( bin.assetSourceRegistry instanceof Object ) { + assetSourceRegistry = bin.assetSourceRegistry; + ubolog('Loaded assetSourceRegistry'); + return assetSourceRegistry; + } } return assets.fetchText( µb.assetsBootstrapLocation || µb.assetsJsonPath @@ -540,6 +543,7 @@ function getAssetSourceRegistry() { : assets.fetchText(µb.assetsJsonPath); }).then(details => { updateAssetSourceRegistry(details.content, true); + ubolog('Loaded assetSourceRegistry'); return assetSourceRegistry; }); }); @@ -670,49 +674,36 @@ let assetCacheRegistryPromise; let assetCacheRegistry = {}; function getAssetCacheRegistry() { - if ( assetCacheRegistryPromise === undefined ) { - assetCacheRegistryPromise = cacheStorage.get( - 'assetCacheRegistry' - ).then(bin => { - if ( - bin instanceof Object && - bin.assetCacheRegistry instanceof Object - ) { - if ( Object.keys(assetCacheRegistry).length === 0 ) { - assetCacheRegistry = bin.assetCacheRegistry; - } else { - console.error( - 'getAssetCacheRegistry(): assetCacheRegistry reassigned!' - ); - if ( - Object.keys(bin.assetCacheRegistry).sort().join() !== - Object.keys(assetCacheRegistry).sort().join() - ) { - console.error( - 'getAssetCacheRegistry(): assetCacheRegistry changes overwritten!' - ); - } - } - } - return assetCacheRegistry; - }); + if ( assetCacheRegistryPromise !== undefined ) { + return assetCacheRegistryPromise; } - + assetCacheRegistryPromise = cacheStorage.get( + 'assetCacheRegistry' + ).then(bin => { + if ( bin instanceof Object === false ) { return; } + if ( bin.assetCacheRegistry instanceof Object === false ) { return; } + if ( Object.keys(assetCacheRegistry).length !== 0 ) { + return console.error('getAssetCacheRegistry(): assetCacheRegistry reassigned!'); + } + ubolog('Loaded assetCacheRegistry'); + assetCacheRegistry = bin.assetCacheRegistry; + }).then(( ) => + assetCacheRegistry + ); return assetCacheRegistryPromise; } const saveAssetCacheRegistry = (( ) => { - const save = function() { + const save = ( ) => { timer.off(); - cacheStorage.set({ assetCacheRegistry }); + return cacheStorage.set({ assetCacheRegistry }); }; const timer = vAPI.defer.create(save); - return function(lazily) { - if ( lazily ) { - timer.offon({ sec: 30 }); - } else { - save(); + return (throttle = 0) => { + if ( throttle === 0 ) { + return save(); } + timer.offon({ sec: throttle }); }; })(); @@ -723,7 +714,9 @@ async function assetCacheRead(assetKey, updateReadTime = false) { const reportBack = function(content) { if ( content instanceof Blob ) { content = ''; } const details = { assetKey, content }; - if ( content === '' ) { details.error = 'ENOTFOUND'; } + if ( content === '' || content === undefined ) { + details.error = 'ENOTFOUND'; + } return details; }; @@ -739,55 +732,39 @@ async function assetCacheRead(assetKey, updateReadTime = false) { ) + ' ms'; } - if ( - bin instanceof Object === false || - bin.hasOwnProperty(internalKey) === false - ) { - return reportBack(''); - } + if ( bin instanceof Object === false ) { return reportBack(''); } + if ( bin.hasOwnProperty(internalKey) === false ) { return reportBack(''); } const entry = assetCacheRegistry[assetKey]; - if ( entry === undefined ) { - return reportBack(''); - } + if ( entry === undefined ) { return reportBack(''); } entry.readTime = Date.now(); if ( updateReadTime ) { - saveAssetCacheRegistry(true); + saveAssetCacheRegistry(23); } return reportBack(bin[internalKey]); } -async function assetCacheWrite(assetKey, details) { - let content = ''; - let options = {}; - if ( typeof details === 'string' ) { - content = details; - } else if ( details instanceof Object ) { - content = details.content || ''; - options = details; - } - - if ( content === '' ) { +async function assetCacheWrite(assetKey, content, options = {}) { + if ( content === '' || content === undefined ) { return assetCacheRemove(assetKey); } const cacheDict = await getAssetCacheRegistry(); - let entry = cacheDict[assetKey]; - if ( entry === undefined ) { - entry = cacheDict[assetKey] = {}; - } + const { resourceTime, url } = options; + const entry = cacheDict[assetKey] || {}; entry.writeTime = entry.readTime = Date.now(); - entry.resourceTime = options.resourceTime || 0; - if ( typeof options.url === 'string' ) { - entry.remoteURL = options.url; + entry.resourceTime = resourceTime || 0; + if ( typeof url === 'string' ) { + entry.remoteURL = url; } - cacheStorage.set({ - assetCacheRegistry, - [`cache/${assetKey}`]: content - }); + cacheDict[assetKey] = entry; + + await cacheStorage.set({ [`cache/${assetKey}`]: content }); + + saveAssetCacheRegistry(3); const result = { assetKey, content }; // https://github.com/uBlockOrigin/uBlock-issues/issues/248 @@ -797,21 +774,31 @@ async function assetCacheWrite(assetKey, details) { return result; } -async function assetCacheRemove(pattern) { +async function assetCacheRemove(pattern, options = {}) { const cacheDict = await getAssetCacheRegistry(); const removedEntries = []; const removedContent = []; for ( const assetKey in cacheDict ) { - if ( pattern instanceof RegExp && !pattern.test(assetKey) ) { - continue; - } - if ( typeof pattern === 'string' && assetKey !== pattern ) { - continue; + if ( pattern instanceof RegExp ) { + if ( pattern.test(assetKey) === false ) { continue; } + } else if ( typeof pattern === 'string' ) { + if ( assetKey !== pattern ) { continue; } } removedEntries.push(assetKey); - removedContent.push('cache/' + assetKey); + removedContent.push(`cache/${assetKey}`); delete cacheDict[assetKey]; } + if ( options.janitor && pattern instanceof RegExp ) { + const re = new RegExp( + pattern.source.replace(/^\^/, '^cache\\/'), + pattern.flags + ); + const keys = await cacheStorage.keys(re); + for ( const key of keys ) { + removedContent.push(key); + ubolog(`Removing stray ${key}`); + } + } if ( removedContent.length !== 0 ) { await Promise.all([ cacheStorage.remove(removedContent), @@ -851,7 +838,7 @@ async function assetCacheSetDetails(assetKey, details) { } } if ( modified ) { - saveAssetCacheRegistry(); + saveAssetCacheRegistry(3); } } @@ -977,8 +964,7 @@ assets.get = async function(assetKey, options = {}) { } if ( details.content === '' ) { continue; } if ( reIsExternalPath.test(contentURL) && options.dontCache !== true ) { - assetCacheWrite(assetKey, { - content: details.content, + assetCacheWrite(assetKey, details.content, { url: contentURL, silent: options.silent === true, }); @@ -1054,8 +1040,7 @@ async function getRemote(assetKey, options = {}) { } // Success - assetCacheWrite(assetKey, { - content: result.content, + assetCacheWrite(assetKey, result.content, { url: contentURL, resourceTime: result.resourceTime || 0, }); @@ -1098,6 +1083,17 @@ assets.put = async function(assetKey, content) { /******************************************************************************/ +assets.toCache = async function(assetKey, content) { + return assetCacheWrite(assetKey, content); +}; + +assets.fromCache = async function(assetKey) { + const details = await assetCacheRead(assetKey); + return details && details.content; +}; + +/******************************************************************************/ + assets.metadata = async function() { await Promise.all([ getAssetSourceRegistry(), @@ -1144,8 +1140,8 @@ assets.metadata = async function() { assets.purge = assetCacheMarkAsDirty; -assets.remove = function(pattern) { - return assetCacheRemove(pattern); +assets.remove = function(...args) { + return assetCacheRemove(...args); }; assets.rmrf = function() { @@ -1297,8 +1293,7 @@ async function diffUpdater() { 'Diff-Path', 'Diff-Expires', ]); - assetCacheWrite(data.assetKey, { - content: data.text, + assetCacheWrite(data.assetKey, data.text, { resourceTime: metadata.lastModified || 0, }); metadata.diffUpdated = true; @@ -1330,6 +1325,8 @@ async function diffUpdater() { terminate(); }; const worker = new Worker('js/diff-updater.js'); + }).catch(reason => { + ubolog(`Diff updater: ${reason}`); }); } diff --git a/src/js/background.js b/src/js/background.js index 578d8a6..edeac08 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -19,22 +19,18 @@ Home: https://github.com/gorhill/uBlock */ -/* globals browser */ - -'use strict'; - /******************************************************************************/ -import logger from './logger.js'; -import { FilteringContext } from './filtering-context.js'; -import { ubologSet } from './console.js'; - import { domainFromHostname, hostnameFromURI, originFromURI, } from './uri-utils.js'; +import { FilteringContext } from './filtering-context.js'; +import logger from './logger.js'; +import { ubologSet } from './console.js'; + /******************************************************************************/ // Not all platforms may have properly declared vAPI.webextFlavor. @@ -49,13 +45,14 @@ const hiddenSettingsDefault = { allowGenericProceduralFilters: false, assetFetchTimeout: 30, autoCommentFilterTemplate: '{{date}} {{origin}}', - autoUpdateAssetFetchPeriod: 15, - autoUpdateDelayAfterLaunch: 105, + autoUpdateAssetFetchPeriod: 5, + autoUpdateDelayAfterLaunch: 37, autoUpdatePeriod: 1, benchmarkDatasetURL: 'unset', blockingProfiles: '11111/#F00 11010/#C0F 11001/#00F 00001', - cacheStorageAPI: 'unset', cacheStorageCompression: true, + cacheStorageCompressionThreshold: 65536, + cacheStorageMultithread: 2, cacheControlForFirefox1376932: 'no-cache, no-store, must-revalidate', cloudStorageCompression: true, cnameIgnoreList: 'unset', @@ -78,10 +75,12 @@ const hiddenSettingsDefault = { modifyWebextFlavor: 'unset', popupFontSize: 'unset', popupPanelDisabledSections: 0, - popupPanelLockedSections: 0, popupPanelHeightMode: 0, + popupPanelLockedSections: 0, + popupPanelOrientation: 'unset', requestJournalProcessPeriod: 1000, - selfieAfter: 2, + requestStatsDisabled: false, + selfieDelayInSeconds: 53, strictBlockingBypassDuration: 120, toolbarWarningTimeout: 60, trustedListPrefixes: 'ublock-', @@ -93,7 +92,7 @@ const hiddenSettingsDefault = { if ( vAPI.webextFlavor.soup.has('devbuild') ) { hiddenSettingsDefault.consoleLogLevel = 'info'; - hiddenSettingsDefault.trustedListPrefixes += ' user-'; + hiddenSettingsDefault.cacheStorageAPI = 'unset'; ubologSet(true); } @@ -112,7 +111,7 @@ const userSettingsDefault = { externalLists: '', firewallPaneMinimized: true, hyperlinkAuditingDisabled: true, - ignoreGenericCosmeticFilters: vAPI.webextFlavor.soup.has('mobile'), + ignoreGenericCosmeticFilters: false, importedLists: [], largeMediaSize: 50, parseAllABPHideFilters: true, @@ -122,6 +121,7 @@ const userSettingsDefault = { showIconBadge: true, suspendUntilListsAreLoaded: vAPI.Net.canSuspend(), tooltipsDisabled: false, + userFiltersTrusted: false, webrtcIPAddressHidden: false, }; @@ -144,7 +144,7 @@ if ( vAPI.webextFlavor.soup.has('firefox') ) { } const µBlock = { // jshint ignore:line - wakeupReason: '', + alarmQueue: [], userSettingsDefault, userSettings: Object.assign({}, userSettingsDefault), @@ -168,26 +168,19 @@ const µBlock = { // jshint ignore:line netWhitelist: new Map(), netWhitelistModifyTime: 0, netWhitelistDefault: [ - 'about-scheme', 'chrome-extension-scheme', - 'chrome-scheme', - 'edge-scheme', 'moz-extension-scheme', - 'opera-scheme', - 'vivaldi-scheme', - 'wyciwyg-scheme', // Firefox's "What-You-Cache-Is-What-You-Get" ], - localSettings: { - blockedRequestCount: 0, - allowedRequestCount: 0, + requestStats: { + blockedCount: 0, + allowedCount: 0, }, - localSettingsLastModified: 0, // Read-only systemSettings: { compiledMagic: 57, // Increase when compiled format changes - selfieMagic: 57, // Increase when selfie format changes + selfieMagic: 58, // Increase when selfie format changes }, // https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501 @@ -311,7 +304,6 @@ const µBlock = { // jshint ignore:line } this.fromTabId(tabId); // Must be called AFTER tab context management this.realm = ''; - this.id = details.requestId; this.setMethod(details.method); this.setURL(details.url); this.aliasURL = details.aliasURL || undefined; @@ -373,8 +365,7 @@ const µBlock = { // jshint ignore:line toLogger() { const details = { - id: this.id, - tstamp: Date.now(), + tstamp: 0, realm: this.realm, method: this.getMethodName(), type: this.stype, diff --git a/src/js/base64-custom.js b/src/js/base64-custom.js index 34141b8..0d9a43f 100644 --- a/src/js/base64-custom.js +++ b/src/js/base64-custom.js @@ -46,105 +46,6 @@ const digitToVal = new Uint8Array(128); } } -// The sparse base64 codec is best for buffers which contains a lot of -// small u32 integer values. Those small u32 integer values are better -// represented with stringified integers, because small values can be -// represented with fewer bits than the usual base64 codec. For example, -// 0 become '0 ', i.e. 16 bits instead of 48 bits with official base64 -// codec. - -const sparseBase64 = { - magic: 'Base64_1', - - encode: function(arrbuf, arrlen) { - const inputLength = (arrlen + 3) >>> 2; - const inbuf = new Uint32Array(arrbuf, 0, inputLength); - const outputLength = this.magic.length + 7 + inputLength * 7; - const outbuf = new Uint8Array(outputLength); - // magic bytes - let j = 0; - for ( let i = 0; i < this.magic.length; i++ ) { - outbuf[j++] = this.magic.charCodeAt(i); - } - // array size - let v = inputLength; - do { - outbuf[j++] = valToDigit[v & 0b111111]; - v >>>= 6; - } while ( v !== 0 ); - outbuf[j++] = 0x20 /* ' ' */; - // array content - for ( let i = 0; i < inputLength; i++ ) { - v = inbuf[i]; - do { - outbuf[j++] = valToDigit[v & 0b111111]; - v >>>= 6; - } while ( v !== 0 ); - outbuf[j++] = 0x20 /* ' ' */; - } - if ( typeof TextDecoder === 'undefined' ) { - return JSON.stringify( - Array.from(new Uint32Array(outbuf.buffer, 0, j >>> 2)) - ); - } - const textDecoder = new TextDecoder(); - return textDecoder.decode(new Uint8Array(outbuf.buffer, 0, j)); - }, - - decode: function(instr, arrbuf) { - if ( instr.charCodeAt(0) === 0x5B /* '[' */ ) { - const inbuf = JSON.parse(instr); - if ( arrbuf instanceof ArrayBuffer === false ) { - return new Uint32Array(inbuf); - } - const outbuf = new Uint32Array(arrbuf); - outbuf.set(inbuf); - return outbuf; - } - if ( instr.startsWith(this.magic) === false ) { - throw new Error('Invalid µBlock.base64 encoding'); - } - const inputLength = instr.length; - const outputLength = this.decodeSize(instr) >> 2; - const outbuf = arrbuf instanceof ArrayBuffer === false - ? new Uint32Array(outputLength) - : new Uint32Array(arrbuf); - let i = instr.indexOf(' ', this.magic.length) + 1; - if ( i === -1 ) { - throw new Error('Invalid µBlock.base64 encoding'); - } - // array content - let j = 0; - for (;;) { - if ( j === outputLength || i >= inputLength ) { break; } - let v = 0, l = 0; - for (;;) { - const c = instr.charCodeAt(i++); - if ( c === 0x20 /* ' ' */ ) { break; } - v += digitToVal[c] << l; - l += 6; - } - outbuf[j++] = v; - } - if ( i < inputLength || j < outputLength ) { - throw new Error('Invalid µBlock.base64 encoding'); - } - return outbuf; - }, - - decodeSize: function(instr) { - if ( instr.startsWith(this.magic) === false ) { return 0; } - let v = 0, l = 0, i = this.magic.length; - for (;;) { - const c = instr.charCodeAt(i++); - if ( c === 0x20 /* ' ' */ ) { break; } - v += digitToVal[c] << l; - l += 6; - } - return v << 2; - }, -}; - // The dense base64 codec is best for typed buffers which values are // more random. For example, buffer contents as a result of compression // contain less repetitive values and thus the content is more @@ -154,7 +55,7 @@ const sparseBase64 = { // ArrayBuffer fails, the content of the resulting Uint8Array is // non-sensical. WASM-related? -const denseBase64 = { +export const denseBase64 = { magic: 'DenseBase64_1', encode: function(input) { @@ -242,5 +143,3 @@ const denseBase64 = { }; /******************************************************************************/ - -export { denseBase64, sparseBase64 }; diff --git a/src/js/benchmarks.js b/src/js/benchmarks.js index 8792f03..9fdc6ec 100644 --- a/src/js/benchmarks.js +++ b/src/js/benchmarks.js @@ -74,8 +74,8 @@ const loadBenchmarkDataset = (( ) => { datasetPromise = undefined; }); - return function() { - ttlTimer.offon({ min: 5 }); + return async function() { + ttlTimer.offon({ min: 2 }); if ( datasetPromise !== undefined ) { return datasetPromise; @@ -84,7 +84,7 @@ const loadBenchmarkDataset = (( ) => { const datasetURL = µb.hiddenSettings.benchmarkDatasetURL; if ( datasetURL === 'unset' ) { console.info(`No benchmark dataset available.`); - return Promise.resolve(); + return; } console.info(`Loading benchmark dataset...`); datasetPromise = io.fetchText(datasetURL).then(details => { @@ -136,7 +136,7 @@ const loadBenchmarkDataset = (( ) => { // action: 1=test -µb.benchmarkStaticNetFiltering = async function(options = {}) { +export async function benchmarkStaticNetFiltering(options = {}) { const { target, redirectEngine } = options; const requests = await loadBenchmarkDataset(); @@ -231,11 +231,11 @@ const loadBenchmarkDataset = (( ) => { const s = output.join('\n'); console.info(s); return s; -}; +} /******************************************************************************/ -µb.tokenHistograms = async function() { +export async function tokenHistogramsfunction() { const requests = await loadBenchmarkDataset(); if ( Array.isArray(requests) === false || requests.length === 0 ) { console.info('No requests found to benchmark'); @@ -272,11 +272,11 @@ const loadBenchmarkDataset = (( ) => { const tophits = Array.from(hitTokenMap).sort(customSort).slice(0, 100); console.info('Misses:', JSON.stringify(topmisses)); console.info('Hits:', JSON.stringify(tophits)); -}; +} /******************************************************************************/ -µb.benchmarkDynamicNetFiltering = async function() { +export async function benchmarkDynamicNetFiltering() { const requests = await loadBenchmarkDataset(); if ( Array.isArray(requests) === false || requests.length === 0 ) { console.info('No requests found to benchmark'); @@ -299,17 +299,19 @@ const loadBenchmarkDataset = (( ) => { const dur = t1 - t0; console.info(`Evaluated ${requests.length} requests in ${dur.toFixed(0)} ms`); console.info(`\tAverage: ${(dur / requests.length).toFixed(3)} ms per request`); -}; +} /******************************************************************************/ -µb.benchmarkCosmeticFiltering = async function() { +export async function benchmarkCosmeticFiltering() { const requests = await loadBenchmarkDataset(); if ( Array.isArray(requests) === false || requests.length === 0 ) { console.info('No requests found to benchmark'); return; } - console.info('Benchmarking cosmeticFilteringEngine.retrieveSpecificSelectors()...'); + const output = [ + 'Benchmarking cosmeticFilteringEngine.retrieveSpecificSelectors()...', + ]; const details = { tabId: undefined, frameId: undefined, @@ -320,6 +322,7 @@ const loadBenchmarkDataset = (( ) => { const options = { noSpecificCosmeticFiltering: false, noGenericCosmeticFiltering: false, + dontInject: true, }; let count = 0; const t0 = performance.now(); @@ -334,25 +337,33 @@ const loadBenchmarkDataset = (( ) => { } const t1 = performance.now(); const dur = t1 - t0; - console.info(`Evaluated ${count} requests in ${dur.toFixed(0)} ms`); - console.info(`\tAverage: ${(dur / count).toFixed(3)} ms per request`); -}; + output.push( + `Evaluated ${count} retrieval in ${dur.toFixed(0)} ms`, + `\tAverage: ${(dur / count).toFixed(3)} ms per document` + ); + const s = output.join('\n'); + console.info(s); + return s; +} /******************************************************************************/ -µb.benchmarkScriptletFiltering = async function() { +export async function benchmarkScriptletFiltering() { const requests = await loadBenchmarkDataset(); if ( Array.isArray(requests) === false || requests.length === 0 ) { console.info('No requests found to benchmark'); return; } - console.info('Benchmarking scriptletFilteringEngine.retrieve()...'); + const output = [ + 'Benchmarking scriptletFilteringEngine.retrieve()...', + ]; const details = { domain: '', entity: '', hostname: '', tabId: 0, url: '', + nocache: true, }; let count = 0; const t0 = performance.now(); @@ -368,13 +379,18 @@ const loadBenchmarkDataset = (( ) => { } const t1 = performance.now(); const dur = t1 - t0; - console.info(`Evaluated ${count} requests in ${dur.toFixed(0)} ms`); - console.info(`\tAverage: ${(dur / count).toFixed(3)} ms per request`); -}; + output.push( + `Evaluated ${count} retrieval in ${dur.toFixed(0)} ms`, + `\tAverage: ${(dur / count).toFixed(3)} ms per document` + ); + const s = output.join('\n'); + console.info(s); + return s; +} /******************************************************************************/ -µb.benchmarkOnBeforeRequest = async function() { +export async function benchmarkOnBeforeRequest() { const requests = await loadBenchmarkDataset(); if ( Array.isArray(requests) === false || requests.length === 0 ) { console.info('No requests found to benchmark'); @@ -416,6 +432,6 @@ const loadBenchmarkDataset = (( ) => { console.info(`\tBlocked ${blockCount} requests`); console.info(`\tAverage: ${(dur / requests.length).toFixed(3)} ms per request`); }); -}; +} /******************************************************************************/ diff --git a/src/js/biditrie.js b/src/js/biditrie.js index d0f64ee..1329316 100644 --- a/src/js/biditrie.js +++ b/src/js/biditrie.js @@ -576,34 +576,19 @@ class BidiTrieContainer { }; } - serialize(encoder) { - if ( encoder instanceof Object ) { - return encoder.encode( - this.buf32.buffer, - this.buf32[CHAR1_SLOT] - ); - } - return Array.from( - new Uint32Array( - this.buf32.buffer, - 0, - this.buf32[CHAR1_SLOT] + 3 >>> 2 - ) + toSelfie() { + return this.buf32.subarray( + 0, + this.buf32[CHAR1_SLOT] + 3 >>> 2 ); } - unserialize(selfie, decoder) { - const shouldDecode = typeof selfie === 'string'; - let byteLength = shouldDecode - ? decoder.decodeSize(selfie) - : selfie.length << 2; + fromSelfie(selfie) { + if ( selfie instanceof Uint32Array === false ) { return false; } + let byteLength = selfie.length << 2; if ( byteLength === 0 ) { return false; } this.reallocateBuf(byteLength); - if ( shouldDecode ) { - decoder.decode(selfie, this.buf8.buffer); - } else { - this.buf32.set(selfie); - } + this.buf32.set(selfie); return true; } diff --git a/src/js/broadcast.js b/src/js/broadcast.js index 0bef46c..61d647f 100644 --- a/src/js/broadcast.js +++ b/src/js/broadcast.js @@ -19,9 +19,7 @@ Home: https://github.com/gorhill/uBlock */ -/* globals browser */ - -'use strict'; +import webext from './webext.js'; /******************************************************************************/ @@ -47,7 +45,7 @@ export async function broadcastToAll(message) { }); const bcmessage = Object.assign({ broadcast: true }, message); for ( const tab of tabs ) { - browser.tabs.sendMessage(tab.id, bcmessage); + webext.tabs.sendMessage(tab.id, bcmessage).catch(( ) => { }); } } @@ -69,7 +67,19 @@ export function filteringBehaviorChanged(details = {}) { } filteringBehaviorChanged.throttle = vAPI.defer.create(( ) => { + const { history, max } = filteringBehaviorChanged; + const now = (Date.now() / 1000) | 0; + if ( history.length >= max ) { + if ( (now - history[0]) <= (10 * 60) ) { return; } + history.shift(); + } + history.push(now); vAPI.net.handlerBehaviorChanged(); }); +filteringBehaviorChanged.history = []; +filteringBehaviorChanged.max = Math.min( + browser.webRequest.MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES - 1, + 19 +); /******************************************************************************/ diff --git a/src/js/cachestorage.js b/src/js/cachestorage.js index ef056af..19f2dae 100644 --- a/src/js/cachestorage.js +++ b/src/js/cachestorage.js @@ -19,191 +19,439 @@ Home: https://github.com/gorhill/uBlock */ -/* global browser, IDBDatabase, indexedDB */ +/* global indexedDB */ 'use strict'; /******************************************************************************/ import lz4Codec from './lz4.js'; -import µb from './background.js'; import webext from './webext.js'; +import µb from './background.js'; +import { ubolog } from './console.js'; +import * as s14e from './s14e-serializer.js'; /******************************************************************************/ -// The code below has been originally manually imported from: -// Commit: https://github.com/nikrolls/uBlock-Edge/commit/d1538ea9bea89d507219d3219592382eee306134 -// Commit date: 29 October 2016 -// Commit author: https://github.com/nikrolls -// Commit message: "Implement cacheStorage using IndexedDB" - -// The original imported code has been subsequently modified as it was not -// compatible with Firefox. -// (a Promise thing, see https://github.com/dfahlander/Dexie.js/issues/317) -// Furthermore, code to migrate from browser.storage.local to vAPI.storage -// has been added, for seamless migration of cache-related entries into -// indexedDB. - -// https://bugzilla.mozilla.org/show_bug.cgi?id=1371255 -// Firefox-specific: we use indexedDB because browser.storage.local() has -// poor performance in Firefox. -// https://github.com/uBlockOrigin/uBlock-issues/issues/328 -// Use IndexedDB for Chromium as well, to take advantage of LZ4 -// compression. -// https://github.com/uBlockOrigin/uBlock-issues/issues/399 -// Revert Chromium support of IndexedDB, use advanced setting to force -// IndexedDB. -// https://github.com/uBlockOrigin/uBlock-issues/issues/409 -// Allow forcing the use of webext storage on Firefox. - const STORAGE_NAME = 'uBlock0CacheStorage'; +const extensionStorage = webext.storage.local; + +const keysFromGetArg = arg => { + if ( arg === null || arg === undefined ) { return []; } + const type = typeof arg; + if ( type === 'string' ) { return [ arg ]; } + if ( Array.isArray(arg) ) { return arg; } + if ( type !== 'object' ) { return; } + return Object.keys(arg); +}; -// Default to webext storage. -const storageLocal = webext.storage.local; - -let storageReadyResolve; -const storageReadyPromise = new Promise(resolve => { - storageReadyResolve = resolve; -}); - -const cacheStorage = { - name: 'browser.storage.local', - get(...args) { - return storageReadyPromise.then(( ) => - storageLocal.get(...args).catch(reason => { - console.log(reason); - }) - ); - }, - set(...args) { - return storageReadyPromise.then(( ) => - storageLocal.set(...args).catch(reason => { - console.log(reason); - }) - ); - }, - remove(...args) { - return storageReadyPromise.then(( ) => - storageLocal.remove(...args).catch(reason => { - console.log(reason); - }) - ); - }, - clear(...args) { - return storageReadyPromise.then(( ) => - storageLocal.clear(...args).catch(reason => { - console.log(reason); - }) - ); - }, - select: function(selectedBackend) { - let actualBackend = selectedBackend; - if ( actualBackend === undefined || actualBackend === 'unset' ) { - actualBackend = vAPI.webextFlavor.soup.has('firefox') - ? 'indexedDB' - : 'browser.storage.local'; - } - if ( actualBackend === 'indexedDB' ) { - return selectIDB().then(success => { - if ( success || selectedBackend === 'indexedDB' ) { - clearWebext(); - storageReadyResolve(); - return 'indexedDB'; +let fastCache = 'indexedDB'; + +/******************************************************************************* + * + * Extension storage + * + * Always available. + * + * */ + +const cacheStorage = (( ) => { + + const exGet = (api, wanted, outbin) => { + return api.get(wanted).then(inbin => { + inbin = inbin || {}; + const found = Object.keys(inbin); + Object.assign(outbin, inbin); + if ( found.length === wanted.length ) { return; } + const missing = []; + for ( const key of wanted ) { + if ( outbin.hasOwnProperty(key) ) { continue; } + missing.push(key); + } + return missing; + }); + }; + + const compress = async (bin, key, data) => { + const µbhs = µb.hiddenSettings; + const after = await s14e.serializeAsync(data, { + compress: µbhs.cacheStorageCompression, + compressThreshold: µbhs.cacheStorageCompressionThreshold, + multithreaded: µbhs.cacheStorageMultithread, + }); + bin[key] = after; + }; + + const decompress = async (bin, key) => { + const data = bin[key]; + if ( s14e.isSerialized(data) === false ) { return; } + const µbhs = µb.hiddenSettings; + const isLarge = data.length >= µbhs.cacheStorageCompressionThreshold; + bin[key] = await s14e.deserializeAsync(data, { + multithreaded: isLarge && µbhs.cacheStorageMultithread || 1, + }); + }; + + const api = { + get(argbin) { + const outbin = {}; + return exGet( + cacheAPIs[fastCache], + keysFromGetArg(argbin), + outbin + ).then(wanted => { + if ( wanted === undefined ) { return; } + return exGet(extensionStorage, wanted, outbin); + }).then(wanted => { + if ( wanted === undefined ) { return; } + if ( argbin instanceof Object === false ) { return; } + if ( Array.isArray(argbin) ) { return; } + for ( const key of wanted ) { + if ( argbin.hasOwnProperty(key) === false ) { continue; } + outbin[key] = argbin[key]; + } + }).then(( ) => { + const promises = []; + for ( const key of Object.keys(outbin) ) { + promises.push(decompress(outbin, key)); } - clearIDB(); - storageReadyResolve(); - return 'browser.storage.local'; + return Promise.all(promises).then(( ) => outbin); + }).catch(reason => { + ubolog(reason); }); - } - if ( actualBackend === 'browser.storage.local' ) { - clearIDB(); - } - storageReadyResolve(); - return Promise.resolve('browser.storage.local'); - - }, - error: undefined -}; + }, + + async keys(regex) { + const results = await Promise.all([ + cacheAPIs[fastCache].keys(regex), + extensionStorage.get(null).catch(( ) => {}), + ]); + const keys = new Set(results[0]); + const bin = results[1] || {}; + for ( const key of Object.keys(bin) ) { + if ( regex && regex.test(key) === false ) { continue; } + keys.add(key); + } + return keys; + }, + + async set(rawbin) { + const keys = Object.keys(rawbin); + if ( keys.length === 0 ) { return; } + const serializedbin = {}; + const promises = []; + for ( const key of keys ) { + promises.push(compress(serializedbin, key, rawbin[key])); + } + await Promise.all(promises); + cacheAPIs[fastCache].set(rawbin, serializedbin); + return extensionStorage.set(serializedbin).catch(reason => { + ubolog(reason); + }); + }, -// Not all platforms support getBytesInUse -if ( storageLocal.getBytesInUse instanceof Function ) { - cacheStorage.getBytesInUse = function(...args) { - return storageLocal.getBytesInUse(...args).catch(reason => { - console.log(reason); - }); + remove(...args) { + cacheAPIs[fastCache].remove(...args); + return extensionStorage.remove(...args).catch(reason => { + ubolog(reason); + }); + }, + + clear(...args) { + cacheAPIs[fastCache].clear(...args); + return extensionStorage.clear(...args).catch(reason => { + ubolog(reason); + }); + }, + + select(api) { + if ( cacheAPIs.hasOwnProperty(api) === false ) { return fastCache; } + fastCache = api; + for ( const k of Object.keys(cacheAPIs) ) { + if ( k === api ) { continue; } + cacheAPIs[k]['clear'](); + } + return fastCache; + }, }; -} -// Reassign API entries to that of indexedDB-based ones -const selectIDB = async function() { - let db; - let dbPromise; + // Not all platforms support getBytesInUse + if ( extensionStorage.getBytesInUse instanceof Function ) { + api.getBytesInUse = function(...args) { + return extensionStorage.getBytesInUse(...args).catch(reason => { + ubolog(reason); + }); + }; + } - const noopfn = function () { + return api; +})(); + +/******************************************************************************* + * + * Cache API + * + * Purpose is to mirror cache-related items from extension storage, as its + * read/write operations are faster. May not be available/populated in + * private/incognito mode. + * + * */ + +const cacheAPI = (( ) => { + const caches = globalThis.caches; + let cacheStoragePromise; + + const getAPI = ( ) => { + if ( cacheStoragePromise !== undefined ) { return cacheStoragePromise; } + cacheStoragePromise = new Promise(resolve => { + if ( typeof caches !== 'object' || caches === null ) { + ubolog('CacheStorage API not available'); + resolve(null); + return; + } + resolve(caches.open(STORAGE_NAME)); + }).catch(reason => { + ubolog(reason); + return null; + }); + return cacheStoragePromise; }; - const disconnect = function() { - dbTimer.off(); - if ( db instanceof IDBDatabase ) { - db.close(); - db = undefined; + const urlPrefix = 'https://ublock0.invalid/'; + + const keyToURL = key => + `${urlPrefix}${encodeURIComponent(key)}`; + + const urlToKey = url => + decodeURIComponent(url.slice(urlPrefix.length)); + + // Cache API is subject to quota so we will use it only for what is key + // performance-wise + const shouldCache = bin => { + const out = {}; + for ( const key of Object.keys(bin) ) { + if ( key.startsWith('cache/' ) ) { + if ( /^cache\/(compiled|selfie)\//.test(key) === false ) { continue; } + } + out[key] = bin[key]; } + if ( Object.keys(out).length !== 0 ) { return out; } }; - const dbTimer = vAPI.defer.create(( ) => { - disconnect(); - }); + const getOne = async key => { + const cache = await getAPI(); + if ( cache === null ) { return; } + return cache.match(keyToURL(key)).then(response => { + if ( response === undefined ) { return; } + return response.text(); + }).then(text => { + if ( text === undefined ) { return; } + return { key, text }; + }).catch(reason => { + ubolog(reason); + }); + }; - const keepAlive = function() { - dbTimer.offon(Math.max( - µb.hiddenSettings.autoUpdateAssetFetchPeriod * 2 * 1000, - 180000 - )); + const getAll = async ( ) => { + const cache = await getAPI(); + if ( cache === null ) { return; } + return cache.keys().then(requests => { + const promises = []; + for ( const request of requests ) { + promises.push(getOne(urlToKey(request.url))); + } + return Promise.all(promises); + }).then(responses => { + const bin = {}; + for ( const response of responses ) { + if ( response === undefined ) { continue; } + bin[response.key] = response.text; + } + return bin; + }).catch(reason => { + ubolog(reason); + }); }; - // https://github.com/gorhill/uBlock/issues/3156 - // I have observed that no event was fired in Tor Browser 7.0.7 + - // medium security level after the request to open the database was - // created. When this occurs, I have also observed that the `error` - // property was already set, so this means uBO can detect here whether - // the database can be opened successfully. A try-catch block is - // necessary when reading the `error` property because we are not - // allowed to read this property outside of event handlers in newer - // implementation of IDBRequest (my understanding). + const setOne = async (key, text) => { + if ( text === undefined ) { return removeOne(key); } + const blob = new Blob([ text ], { type: 'text/plain;charset=utf-8'}); + const cache = await getAPI(); + if ( cache === null ) { return; } + return cache + .put(keyToURL(key), new Response(blob)) + .catch(reason => { + ubolog(reason); + }); + }; - const getDb = function() { - keepAlive(); - if ( db !== undefined ) { - return Promise.resolve(db); - } - if ( dbPromise !== undefined ) { - return dbPromise; - } - dbPromise = new Promise(resolve => { - let req; - try { - req = indexedDB.open(STORAGE_NAME, 1); - if ( req.error ) { - console.log(req.error); - req = undefined; + const removeOne = async key => { + const cache = await getAPI(); + if ( cache === null ) { return; } + return cache.delete(keyToURL(key)).catch(reason => { + ubolog(reason); + }); + }; + + return { + async get(arg) { + const keys = keysFromGetArg(arg); + if ( keys === undefined ) { return; } + if ( keys.length === 0 ) { + return getAll(); + } + const bin = {}; + const toFetch = keys.slice(); + const hasDefault = typeof arg === 'object' && Array.isArray(arg) === false; + for ( let i = 0; i < toFetch.length; i++ ) { + const key = toFetch[i]; + if ( hasDefault && arg[key] !== undefined ) { + bin[key] = arg[key]; } - } catch(ex) { + toFetch[i] = getOne(key); } - if ( req === undefined ) { - db = null; - dbPromise = undefined; - return resolve(null); + const responses = await Promise.all(toFetch); + for ( const response of responses ) { + if ( response === undefined ) { continue; } + const { key, text } = response; + if ( typeof key !== 'string' ) { continue; } + if ( typeof text !== 'string' ) { continue; } + bin[key] = text; } - req.onupgradeneeded = function(ev) { - // https://github.com/uBlockOrigin/uBlock-issues/issues/2725 - // If context Firefox + incognito mode, fall back to - // browser.storage.local for cache storage purpose. - if ( - vAPI.webextFlavor.soup.has('firefox') && - browser.extension.inIncognitoContext === true - ) { - return req.onerror(); + if ( Object.keys(bin).length === 0 ) { return; } + return bin; + }, + + async keys(regex) { + const cache = await getAPI(); + if ( cache === null ) { return []; } + return cache.keys().then(requests => + requests.map(r => urlToKey(r.url)) + .filter(k => regex === undefined || regex.test(k)) + ).catch(( ) => []); + }, + + async set(rawbin, serializedbin) { + const bin = shouldCache(serializedbin); + if ( bin === undefined ) { return; } + const keys = Object.keys(bin); + const promises = []; + for ( const key of keys ) { + promises.push(setOne(key, bin[key])); + } + return Promise.all(promises); + }, + + remove(keys) { + const toRemove = []; + if ( typeof keys === 'string' ) { + toRemove.push(removeOne(keys)); + } else if ( Array.isArray(keys) ) { + for ( const key of keys ) { + toRemove.push(removeOne(key)); } + } + return Promise.all(toRemove); + }, + + async clear() { + if ( typeof caches !== 'object' || caches === null ) { return; } + return globalThis.caches.delete(STORAGE_NAME).catch(reason => { + ubolog(reason); + }); + }, + + shutdown() { + cacheStoragePromise = undefined; + return this.clear(); + }, + }; +})(); + +/******************************************************************************* + * + * In-memory storage + * + * */ + +const memoryStorage = (( ) => { + + const sessionStorage = vAPI.sessionStorage; + + // This should help speed up loading from suspended state in Firefox for + // Android. + // 20240228 Observation: Slows down loading from suspended state in + // Firefox desktop. Could be different in Firefox for Android. + const shouldCache = bin => { + const out = {}; + for ( const key of Object.keys(bin) ) { + if ( key.startsWith('cache/compiled/') ) { continue; } + out[key] = bin[key]; + } + if ( Object.keys(out).length !== 0 ) { return out; } + }; + + return { + get(...args) { + return sessionStorage.get(...args).then(bin => { + return bin; + }).catch(reason => { + ubolog(reason); + }); + }, + + async keys(regex) { + const bin = await this.get(null); + const keys = []; + for ( const key of Object.keys(bin || {}) ) { + if ( regex && regex.test(key) === false ) { continue; } + keys.push(key); + } + return keys; + }, + + async set(rawbin, serializedbin) { + const bin = shouldCache(serializedbin); + if ( bin === undefined ) { return; } + return sessionStorage.set(bin).catch(reason => { + ubolog(reason); + }); + }, + + remove(...args) { + return sessionStorage.remove(...args).catch(reason => { + ubolog(reason); + }); + }, + + clear(...args) { + return sessionStorage.clear(...args).catch(reason => { + ubolog(reason); + }); + }, + + shutdown() { + return this.clear(); + }, + }; +})(); + +/******************************************************************************* + * + * IndexedDB + * + * Deprecated, exists only for the purpose of migrating from older versions. + * + * */ + +const idbStorage = (( ) => { + let dbPromise; + + const getDb = function() { + if ( dbPromise !== undefined ) { return dbPromise; } + dbPromise = new Promise(resolve => { + const req = indexedDB.open(STORAGE_NAME, 1); + req.onupgradeneeded = ev => { if ( ev.oldVersion === 1 ) { return; } try { const db = ev.target.result; @@ -212,35 +460,44 @@ const selectIDB = async function() { req.onerror(); } }; - req.onsuccess = function(ev) { + req.onsuccess = ev => { if ( resolve === undefined ) { return; } - req = undefined; - db = ev.target.result; - dbPromise = undefined; - resolve(db); + resolve(ev.target.result || null); resolve = undefined; }; - req.onerror = req.onblocked = function() { + req.onerror = req.onblocked = ( ) => { if ( resolve === undefined ) { return; } - req = undefined; - console.log(this.error); - db = null; - dbPromise = undefined; + ubolog(req.error); resolve(null); resolve = undefined; }; - vAPI.defer.once(5000).then(( ) => { + vAPI.defer.once(10000).then(( ) => { if ( resolve === undefined ) { return; } - db = null; - dbPromise = undefined; resolve(null); resolve = undefined; }); + }).catch(reason => { + ubolog(`idbStorage() / getDb() failed: ${reason}`); + return null; }); return dbPromise; }; - const fromBlob = function(data) { + // Cache API is subject to quota so we will use it only for what is key + // performance-wise + const shouldCache = bin => { + const out = {}; + for ( const key of Object.keys(bin) ) { + if ( key.startsWith('cache/' ) ) { + if ( /^cache\/(compiled|selfie)\//.test(key) === false ) { continue; } + } + out[key] = bin[key]; + } + if ( Object.keys(out).length === 0 ) { return; } + return out; + }; + + const fromBlob = data => { if ( data instanceof Blob === false ) { return Promise.resolve(data); } @@ -253,277 +510,213 @@ const selectIDB = async function() { }); }; - const toBlob = function(data) { - const value = data instanceof Uint8Array - ? new Blob([ data ]) - : data; - return Promise.resolve(value); + const decompress = (key, value) => { + return lz4Codec.decode(value, fromBlob).then(value => { + return { key, value }; + }); }; - const compress = function(store, key, data) { - return lz4Codec.encode(data, toBlob).then(value => { - store.push({ key, value }); + const getAllEntries = async function() { + const db = await getDb(); + if ( db === null ) { return []; } + return new Promise(resolve => { + const entries = []; + const transaction = db.transaction(STORAGE_NAME, 'readonly'); + transaction.oncomplete = + transaction.onerror = + transaction.onabort = ( ) => { + resolve(Promise.all(entries)); + }; + const table = transaction.objectStore(STORAGE_NAME); + const req = table.openCursor(); + req.onsuccess = ev => { + const cursor = ev.target && ev.target.result; + if ( !cursor ) { return; } + const { key, value } = cursor.value; + if ( value instanceof Blob ) { + entries.push(decompress(key, value)); + } else { + entries.push({ key, value }); + } + cursor.continue(); + }; + }).catch(reason => { + ubolog(`idbStorage() / getAllEntries() failed: ${reason}`); + return []; }); }; - const decompress = function(store, key, data) { - return lz4Codec.decode(data, fromBlob).then(data => { - store[key] = data; + const getAllKeys = async function(regex) { + const db = await getDb(); + if ( db === null ) { return []; } + return new Promise(resolve => { + const keys = []; + const transaction = db.transaction(STORAGE_NAME, 'readonly'); + transaction.oncomplete = + transaction.onerror = + transaction.onabort = ( ) => { + resolve(keys); + }; + const table = transaction.objectStore(STORAGE_NAME); + const req = table.openCursor(); + req.onsuccess = ev => { + const cursor = ev.target && ev.target.result; + if ( !cursor ) { return; } + if ( regex && regex.test(cursor.key) === false ) { return; } + keys.push(cursor.key); + cursor.continue(); + }; + }).catch(reason => { + ubolog(`idbStorage() / getAllKeys() failed: ${reason}`); + return []; }); }; - const getFromDb = async function(keys, keyvalStore, callback) { - if ( typeof callback !== 'function' ) { return; } - if ( keys.length === 0 ) { return callback(keyvalStore); } - const promises = []; - const gotOne = function() { - if ( typeof this.result !== 'object' ) { return; } - const { key, value } = this.result; - keyvalStore[key] = value; - if ( value instanceof Blob === false ) { return; } - promises.push(decompress(keyvalStore, key, value)); - }; - try { - const db = await getDb(); - if ( !db ) { return callback(); } + const getEntries = async function(keys) { + const db = await getDb(); + if ( db === null ) { return []; } + return new Promise(resolve => { + const entries = []; + const gotOne = ev => { + const { result } = ev.target; + if ( typeof result !== 'object' ) { return; } + if ( result === null ) { return; } + const { key, value } = result; + if ( value instanceof Blob ) { + entries.push(decompress(key, value)); + } else { + entries.push({ key, value }); + } + }; const transaction = db.transaction(STORAGE_NAME, 'readonly'); transaction.oncomplete = transaction.onerror = - transaction.onabort = ( ) => { - Promise.all(promises).then(( ) => { - callback(keyvalStore); - }); + transaction.onabort = ( ) => { + resolve(Promise.all(entries)); }; const table = transaction.objectStore(STORAGE_NAME); for ( const key of keys ) { const req = table.get(key); req.onsuccess = gotOne; - req.onerror = noopfn; + req.onerror = ( ) => { }; } - } - catch(reason) { - console.info(`cacheStorage.getFromDb() failed: ${reason}`); - callback(); - } - }; - - const visitAllFromDb = async function(visitFn) { - const db = await getDb(); - if ( !db ) { return visitFn(); } - const transaction = db.transaction(STORAGE_NAME, 'readonly'); - transaction.oncomplete = - transaction.onerror = - transaction.onabort = ( ) => visitFn(); - const table = transaction.objectStore(STORAGE_NAME); - const req = table.openCursor(); - req.onsuccess = function(ev) { - let cursor = ev.target && ev.target.result; - if ( !cursor ) { return; } - let entry = cursor.value; - visitFn(entry); - cursor.continue(); - }; - }; - - const getAllFromDb = function(callback) { - if ( typeof callback !== 'function' ) { return; } - const promises = []; - const keyvalStore = {}; - visitAllFromDb(entry => { - if ( entry === undefined ) { - Promise.all(promises).then(( ) => { - callback(keyvalStore); - }); - return; - } - const { key, value } = entry; - keyvalStore[key] = value; - if ( entry.value instanceof Blob === false ) { return; } - promises.push(decompress(keyvalStore, key, value)); }).catch(reason => { - console.info(`cacheStorage.getAllFromDb() failed: ${reason}`); - callback(); + ubolog(`idbStorage() / getEntries() failed: ${reason}`); + return []; }); }; - // https://github.com/uBlockOrigin/uBlock-issues/issues/141 - // Mind that IDBDatabase.transaction() and IDBObjectStore.put() - // can throw: - // https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/transaction - // https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/put - - const putToDb = async function(keyvalStore, callback) { - if ( typeof callback !== 'function' ) { - callback = noopfn; + const getAll = async ( ) => { + const entries = await getAllEntries(); + const outbin = {}; + for ( const { key, value } of entries ) { + outbin[key] = value; } - const keys = Object.keys(keyvalStore); - if ( keys.length === 0 ) { return callback(); } - const promises = [ getDb() ]; - const entries = []; - const dontCompress = - µb.hiddenSettings.cacheStorageCompression !== true; - for ( const key of keys ) { - const value = keyvalStore[key]; - const isString = typeof value === 'string'; - if ( isString === false || dontCompress ) { - entries.push({ key, value }); - continue; + return outbin; + }; + + const setEntries = async inbin => { + const keys = Object.keys(inbin); + if ( keys.length === 0 ) { return; } + const db = await getDb(); + if ( db === null ) { return; } + return new Promise(resolve => { + const entries = []; + for ( const key of keys ) { + entries.push({ key, value: inbin[key] }); } - promises.push(compress(entries, key, value)); - } - const finish = ( ) => { - if ( callback === undefined ) { return; } - let cb = callback; - callback = undefined; - cb(); - }; - try { - const results = await Promise.all(promises); - const db = results[0]; - if ( !db ) { return callback(); } - const transaction = db.transaction( - STORAGE_NAME, - 'readwrite' - ); + const transaction = db.transaction(STORAGE_NAME, 'readwrite'); transaction.oncomplete = transaction.onerror = - transaction.onabort = finish; + transaction.onabort = ( ) => { + resolve(); + }; const table = transaction.objectStore(STORAGE_NAME); for ( const entry of entries ) { table.put(entry); } - } catch (ex) { - finish(); - } + }).catch(reason => { + ubolog(`idbStorage() / setEntries() failed: ${reason}`); + }); }; - const deleteFromDb = async function(input, callback) { - if ( typeof callback !== 'function' ) { - callback = noopfn; - } - const keys = Array.isArray(input) ? input.slice() : [ input ]; - if ( keys.length === 0 ) { return callback(); } - const finish = ( ) => { - if ( callback === undefined ) { return; } - let cb = callback; - callback = undefined; - cb(); - }; - try { - const db = await getDb(); - if ( !db ) { return callback(); } + const deleteEntries = async arg => { + const keys = Array.isArray(arg) ? arg.slice() : [ arg ]; + if ( keys.length === 0 ) { return; } + const db = await getDb(); + if ( db === null ) { return; } + return new Promise(resolve => { const transaction = db.transaction(STORAGE_NAME, 'readwrite'); transaction.oncomplete = transaction.onerror = - transaction.onabort = finish; + transaction.onabort = ( ) => { + resolve(); + }; const table = transaction.objectStore(STORAGE_NAME); for ( const key of keys ) { table.delete(key); } - } catch (ex) { - finish(); - } - }; - - const clearDb = async function(callback) { - if ( typeof callback !== 'function' ) { - callback = noopfn; - } - try { - const db = await getDb(); - if ( !db ) { return callback(); } - const transaction = db.transaction(STORAGE_NAME, 'readwrite'); - transaction.oncomplete = - transaction.onerror = - transaction.onabort = ( ) => { - callback(); - }; - transaction.objectStore(STORAGE_NAME).clear(); - } - catch(reason) { - console.info(`cacheStorage.clearDb() failed: ${reason}`); - callback(); - } + }).catch(reason => { + ubolog(`idbStorage() / deleteEntries() failed: ${reason}`); + }); }; - await getDb(); - if ( !db ) { return false; } - - cacheStorage.name = 'indexedDB'; - cacheStorage.get = function get(keys) { - return storageReadyPromise.then(( ) => - new Promise(resolve => { - if ( keys === null ) { - return getAllFromDb(bin => resolve(bin)); - } - let toRead, output = {}; - if ( typeof keys === 'string' ) { - toRead = [ keys ]; - } else if ( Array.isArray(keys) ) { - toRead = keys; - } else /* if ( typeof keys === 'object' ) */ { - toRead = Object.keys(keys); - output = keys; + return { + async get(argbin) { + const keys = keysFromGetArg(argbin); + if ( keys === undefined ) { return; } + if ( keys.length === 0 ) { return getAll(); } + const entries = await getEntries(keys); + const outbin = {}; + for ( const { key, value } of entries ) { + outbin[key] = value; + } + if ( argbin instanceof Object && Array.isArray(argbin) === false ) { + for ( const key of keys ) { + if ( outbin.hasOwnProperty(key) ) { continue; } + outbin[key] = argbin[key]; } - getFromDb(toRead, output, bin => resolve(bin)); - }) - ); - }; - cacheStorage.set = function set(keys) { - return storageReadyPromise.then(( ) => - new Promise(resolve => { - putToDb(keys, details => resolve(details)); - }) - ); - }; - cacheStorage.remove = function remove(keys) { - return storageReadyPromise.then(( ) => - new Promise(resolve => { - deleteFromDb(keys, ( ) => resolve()); - }) - ); - }; - cacheStorage.clear = function clear() { - return storageReadyPromise.then(( ) => - new Promise(resolve => { - clearDb(( ) => resolve()); - }) - ); - }; - cacheStorage.getBytesInUse = function getBytesInUse() { - return Promise.resolve(0); + } + return outbin; + }, + + async set(rawbin) { + const bin = shouldCache(rawbin); + if ( bin === undefined ) { return; } + return setEntries(bin); + }, + + keys(...args) { + return getAllKeys(...args); + }, + + remove(...args) { + return deleteEntries(...args); + }, + + clear() { + return getDb().then(db => { + if ( db === null ) { return; } + db.close(); + indexedDB.deleteDatabase(STORAGE_NAME); + }).catch(reason => { + ubolog(`idbStorage.clear() failed: ${reason}`); + }); + }, + + async shutdown() { + await this.clear(); + dbPromise = undefined; + }, }; - return true; -}; +})(); -// https://github.com/uBlockOrigin/uBlock-issues/issues/328 -// Delete cache-related entries from webext storage. -const clearWebext = async function() { - let bin; - try { - bin = await webext.storage.local.get('assetCacheRegistry'); - } catch(ex) { - console.error(ex); - } - if ( bin instanceof Object === false ) { return; } - if ( bin.assetCacheRegistry instanceof Object === false ) { return; } - const toRemove = [ - 'assetCacheRegistry', - 'assetSourceRegistry', - ]; - for ( const key in bin.assetCacheRegistry ) { - if ( bin.assetCacheRegistry.hasOwnProperty(key) ) { - toRemove.push('cache/' + key); - } - } - webext.storage.local.remove(toRemove); -}; +/******************************************************************************/ -const clearIDB = function() { - try { - indexedDB.deleteDatabase(STORAGE_NAME); - } catch(ex) { - } +const cacheAPIs = { + 'indexedDB': idbStorage, + 'cacheAPI': cacheAPI, + 'browser.storage.session': memoryStorage, }; /******************************************************************************/ diff --git a/src/js/click2load.js b/src/js/click2load.js index 42b7525..b441d97 100644 --- a/src/js/click2load.js +++ b/src/js/click2load.js @@ -49,9 +49,8 @@ document.body.addEventListener('click', ev => { what: 'clickToLoad', frameURL, }).then(ok => { - if ( ok ) { - self.location.replace(frameURL); - } + if ( ok !== true ) { return; } + self.location.replace(frameURL); }); }); diff --git a/src/js/codemirror/search.js b/src/js/codemirror/search.js index 477e9cc..7ee5b33 100644 --- a/src/js/codemirror/search.js +++ b/src/js/codemirror/search.js @@ -25,18 +25,25 @@ // Ctrl-G. // ===== -'use strict'; - import { dom, qs$ } from '../dom.js'; import { i18n$ } from '../i18n.js'; { const CodeMirror = self.CodeMirror; + CodeMirror.defineOption('maximizable', true, (cm, maximizable) => { + if ( typeof maximizable !== 'boolean' ) { return; } + const wrapper = cm.getWrapperElement(); + if ( wrapper === null ) { return; } + const container = wrapper.closest('.codeMirrorContainer'); + if ( container === null ) { return; } + container.dataset.maximizable = `${maximizable}`; + }); + const searchOverlay = function(query, caseInsensitive) { if ( typeof query === 'string' ) query = new RegExp( - query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), + query.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'), caseInsensitive ? 'gi' : 'g' ); else if ( !query.global ) @@ -89,8 +96,10 @@ import { i18n$ } from '../i18n.js'; state.queryTimer.offon(350); }; - const searchWidgetClickHandler = function(cm, ev) { - const tcl = ev.target.classList; + const searchWidgetClickHandler = (ev, cm) => { + if ( ev.button !== 0 ) { return; } + const target = ev.target; + const tcl = target.classList; if ( tcl.contains('cm-search-widget-up') ) { findNext(cm, -1); } else if ( tcl.contains('cm-search-widget-down') ) { @@ -99,11 +108,14 @@ import { i18n$ } from '../i18n.js'; findNextError(cm, -1); } else if ( tcl.contains('cm-linter-widget-down') ) { findNextError(cm, 1); + } else if ( tcl.contains('cm-maximize') ) { + const container = target.closest('.codeMirrorContainer'); + if ( container !== null ) { + container.classList.toggle('cm-maximized'); + } } - if ( ev.target.localName !== 'input' ) { - ev.preventDefault(); - } else { - ev.stopImmediatePropagation(); + if ( target.localName !== 'input' ) { + cm.focus(); } }; @@ -127,7 +139,9 @@ import { i18n$ } from '../i18n.js'; this.widget = widgetParent.children[0]; this.widget.addEventListener('keydown', searchWidgetKeydownHandler.bind(null, cm)); this.widget.addEventListener('input', searchWidgetInputHandler.bind(null, cm)); - this.widget.addEventListener('mousedown', searchWidgetClickHandler.bind(null, cm)); + this.widget.addEventListener('click', ev => { + searchWidgetClickHandler(ev, cm); + }); if ( typeof cm.addPanel === 'function' ) { this.panel = cm.addPanel(this.widget); } @@ -236,10 +250,7 @@ import { i18n$ } from '../i18n.js'; notation: 'compact', maximumSignificantDigits: 3 }); - if ( - intl.resolvedOptions instanceof Function && - intl.resolvedOptions().hasOwnProperty('notation') - ) { + if ( intl.resolvedOptions().notation ) { intlNumberFormat = intl; } } @@ -330,9 +341,6 @@ import { i18n$ } from '../i18n.js'; state.annotate.update(annotations); }); state.widget.setAttribute('data-query', state.queryText); - // Ensure the caret is visible - const input = state.widget.querySelector('.cm-search-widget-input input'); - input.selectionStart = input.selectionStart; }; const findNext = function(cm, dir, callback) { @@ -458,26 +466,30 @@ import { i18n$ } from '../i18n.js'; }; { - const searchWidgetTemplate = - '<div class="cm-search-widget-template" style="display:none;">' + - '<div class="cm-search-widget">' + - '<span class="cm-search-widget-input">' + - '<span class="fa-icon fa-icon-ro">search</span> ' + - '<input type="search" spellcheck="false"> ' + - '<span class="cm-search-widget-up cm-search-widget-button fa-icon">angle-up</span> ' + - '<span class="cm-search-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span> ' + - '<span class="cm-search-widget-count"></span>' + - '</span>' + - '<span class="cm-linter-widget" data-lint="0">' + - '<span class="cm-linter-widget-count"></span> ' + - '<span class="cm-linter-widget-up cm-search-widget-button fa-icon">angle-up</span> ' + - '<span class="cm-linter-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span> ' + - '</span>' + - '<span>' + - '<a class="fa-icon sourceURL" href>external-link</a>' + - '</span>' + - '</div>' + - '</div>'; + const searchWidgetTemplate = [ + '<div class="cm-search-widget-template" style="display:none;">', + '<div class="cm-search-widget">', + '<span class="cm-maximize"><svg viewBox="0 0 40 40"><path d="M4,16V4h12M24,4H36V16M4,24V36H16M36,24V36H24" /><path d="M14 2.5v12h-12M38 14h-12v-12M14 38v-12h-12M26 38v-12h12" /></svg></span> ', + '<span class="cm-search-widget-input">', + '<span class="searchfield">', + '<input type="search" spellcheck="false" placeholder="">', + '<span class="fa-icon">search</span>', + '</span> ', + '<span class="cm-search-widget-up cm-search-widget-button fa-icon">angle-up</span> ', + '<span class="cm-search-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span> ', + '<span class="cm-search-widget-count"></span>', + '</span>', + '<span class="cm-linter-widget" data-lint="0">', + '<span class="cm-linter-widget-count"></span> ', + '<span class="cm-linter-widget-up cm-search-widget-button fa-icon">angle-up</span> ', + '<span class="cm-linter-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span> ', + '</span>', + '<span>', + '<a class="fa-icon sourceURL" href>external-link</a>', + '</span>', + '</div>', + '</div>', + ].join('\n'); const domParser = new DOMParser(); const doc = domParser.parseFromString(searchWidgetTemplate, 'text/html'); const widgetTemplate = document.adoptNode(doc.body.firstElementChild); diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js index ac1b048..2aaf85b 100644 --- a/src/js/codemirror/ubo-static-filtering.js +++ b/src/js/codemirror/ubo-static-filtering.js @@ -21,8 +21,6 @@ /* global CodeMirror */ -'use strict'; - /******************************************************************************/ import * as sfp from '../static-filtering-parser.js'; @@ -39,10 +37,10 @@ let hintHelperRegistered = false; /******************************************************************************/ -CodeMirror.defineOption('trustedSource', false, (cm, state) => { - if ( typeof state !== 'boolean' ) { return; } +CodeMirror.defineOption('trustedSource', false, (cm, trusted) => { + if ( typeof trusted !== 'boolean' ) { return; } self.dispatchEvent(new CustomEvent('trustedSource', { - detail: state, + detail: { cm, trusted }, })); }); @@ -56,219 +54,232 @@ CodeMirror.defineOption('trustedScriptletTokens', undefined, (cm, tokens) => { /******************************************************************************/ -CodeMirror.defineMode('ubo-static-filtering', function() { - const astParser = new sfp.AstFilterParser({ - interactive: true, - nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'), - }); - const astWalker = astParser.getWalker(); - let currentWalkerNode = 0; - let lastNetOptionType = 0; - - const redirectTokenStyle = node => { - const rawToken = astParser.getNodeString(node || currentWalkerNode); +const uBOStaticFilteringMode = (( ) => { + const redirectTokenStyle = (mode, node) => { + const rawToken = mode.astParser.getNodeString(node || mode.currentWalkerNode); const { token } = sfp.parseRedirectValue(rawToken); return redirectNames.has(token) ? 'value' : 'value warning'; }; - const nodeHasError = node => { - return astParser.getNodeFlags( - node || currentWalkerNode, sfp.NODE_FLAG_ERROR + const nodeHasError = (mode, node) => { + return mode.astParser.getNodeFlags( + node || mode.currentWalkerNode, sfp.NODE_FLAG_ERROR ) !== 0; }; - const colorFromAstNode = function() { - if ( astParser.nodeIsEmptyString(currentWalkerNode) ) { return '+'; } - if ( nodeHasError() ) { return 'error'; } - const nodeType = astParser.getNodeType(currentWalkerNode); + const colorFromAstNode = mode => { + if ( mode.astParser.nodeIsEmptyString(mode.currentWalkerNode) ) { return '+'; } + if ( nodeHasError(mode) ) { return 'error'; } + const nodeType = mode.astParser.getNodeType(mode.currentWalkerNode); switch ( nodeType ) { - case sfp.NODE_TYPE_WHITESPACE: - return ''; - case sfp.NODE_TYPE_COMMENT: - if ( astWalker.canGoDown() ) { break; } - return 'comment'; - case sfp.NODE_TYPE_COMMENT_URL: - return 'comment link'; - case sfp.NODE_TYPE_IGNORE: - return 'comment'; - case sfp.NODE_TYPE_PREPARSE_DIRECTIVE: - case sfp.NODE_TYPE_PREPARSE_DIRECTIVE_VALUE: - return 'directive'; - case sfp.NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE: { - const raw = astParser.getNodeString(currentWalkerNode); - const state = sfp.utils.preparser.evaluateExpr(raw, preparseDirectiveEnv); - return state ? 'positive strong' : 'negative strong'; + case sfp.NODE_TYPE_WHITESPACE: + return ''; + case sfp.NODE_TYPE_COMMENT: + if ( mode.astWalker.canGoDown() ) { break; } + return 'comment'; + case sfp.NODE_TYPE_COMMENT_URL: + return 'comment link'; + case sfp.NODE_TYPE_IGNORE: + return 'comment'; + case sfp.NODE_TYPE_PREPARSE_DIRECTIVE: + case sfp.NODE_TYPE_PREPARSE_DIRECTIVE_VALUE: + return 'directive'; + case sfp.NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE: { + const raw = mode.astParser.getNodeString(mode.currentWalkerNode); + const state = sfp.utils.preparser.evaluateExpr(raw, preparseDirectiveEnv); + return state ? 'positive strong' : 'negative strong'; + } + case sfp.NODE_TYPE_EXT_OPTIONS_ANCHOR: + return mode.astParser.getFlags(sfp.AST_FLAG_IS_EXCEPTION) + ? 'tag strong' + : 'def strong'; + case sfp.NODE_TYPE_EXT_DECORATION: + return 'def'; + case sfp.NODE_TYPE_EXT_PATTERN_RAW: + if ( mode.astWalker.canGoDown() ) { break; } + return 'variable'; + case sfp.NODE_TYPE_EXT_PATTERN_COSMETIC: + case sfp.NODE_TYPE_EXT_PATTERN_HTML: + return 'variable'; + case sfp.NODE_TYPE_EXT_PATTERN_RESPONSEHEADER: + case sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET: + if ( mode.astWalker.canGoDown() ) { break; } + return 'variable'; + case sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN: { + const token = mode.astParser.getNodeString(mode.currentWalkerNode); + if ( scriptletNames.has(token) === false ) { + return 'warning'; } - case sfp.NODE_TYPE_EXT_OPTIONS_ANCHOR: - return astParser.getFlags(sfp.AST_FLAG_IS_EXCEPTION) - ? 'tag strong' - : 'def strong'; - case sfp.NODE_TYPE_EXT_DECORATION: - return 'def'; - case sfp.NODE_TYPE_EXT_PATTERN_RAW: - if ( astWalker.canGoDown() ) { break; } - return 'variable'; - case sfp.NODE_TYPE_EXT_PATTERN_COSMETIC: - case sfp.NODE_TYPE_EXT_PATTERN_HTML: - return 'variable'; - case sfp.NODE_TYPE_EXT_PATTERN_RESPONSEHEADER: - case sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET: - if ( astWalker.canGoDown() ) { break; } - return 'variable'; - case sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN: { - const token = astParser.getNodeString(currentWalkerNode); - if ( scriptletNames.has(token) === false ) { - return 'warning'; + return 'variable'; + } + case sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG: + return 'variable'; + case sfp.NODE_TYPE_NET_EXCEPTION: + return 'tag strong'; + case sfp.NODE_TYPE_NET_PATTERN: + if ( mode.astWalker.canGoDown() ) { break; } + if ( mode.astParser.isRegexPattern() ) { + if ( mode.astParser.getNodeFlags(mode.currentWalkerNode, sfp.NODE_FLAG_PATTERN_UNTOKENIZABLE) !== 0 ) { + return 'variable warning'; } - return 'variable'; + return 'variable notice'; } - case sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG: - return 'variable'; - case sfp.NODE_TYPE_NET_EXCEPTION: - return 'tag strong'; - case sfp.NODE_TYPE_NET_PATTERN: - if ( astWalker.canGoDown() ) { break; } - if ( astParser.isRegexPattern() ) { - if ( astParser.getNodeFlags(currentWalkerNode, sfp.NODE_FLAG_PATTERN_UNTOKENIZABLE) !== 0 ) { - return 'variable warning'; - } - return 'variable notice'; - } - return 'variable'; - case sfp.NODE_TYPE_NET_PATTERN_PART: - return 'variable'; - case sfp.NODE_TYPE_NET_PATTERN_PART_SPECIAL: - return 'keyword strong'; - case sfp.NODE_TYPE_NET_PATTERN_PART_UNICODE: - return 'variable unicode'; - case sfp.NODE_TYPE_NET_PATTERN_LEFT_HNANCHOR: - case sfp.NODE_TYPE_NET_PATTERN_LEFT_ANCHOR: - case sfp.NODE_TYPE_NET_PATTERN_RIGHT_ANCHOR: - case sfp.NODE_TYPE_NET_OPTION_NAME_NOT: - return 'keyword strong'; - case sfp.NODE_TYPE_NET_OPTIONS_ANCHOR: - case sfp.NODE_TYPE_NET_OPTION_SEPARATOR: - lastNetOptionType = 0; - return 'def strong'; - case sfp.NODE_TYPE_NET_OPTION_NAME_UNKNOWN: - lastNetOptionType = 0; - return 'error'; - case sfp.NODE_TYPE_NET_OPTION_NAME_1P: - case sfp.NODE_TYPE_NET_OPTION_NAME_STRICT1P: - case sfp.NODE_TYPE_NET_OPTION_NAME_3P: - case sfp.NODE_TYPE_NET_OPTION_NAME_STRICT3P: - case sfp.NODE_TYPE_NET_OPTION_NAME_ALL: - case sfp.NODE_TYPE_NET_OPTION_NAME_BADFILTER: - case sfp.NODE_TYPE_NET_OPTION_NAME_CNAME: - case sfp.NODE_TYPE_NET_OPTION_NAME_CSP: - case sfp.NODE_TYPE_NET_OPTION_NAME_CSS: - case sfp.NODE_TYPE_NET_OPTION_NAME_DENYALLOW: - case sfp.NODE_TYPE_NET_OPTION_NAME_DOC: - case sfp.NODE_TYPE_NET_OPTION_NAME_EHIDE: - case sfp.NODE_TYPE_NET_OPTION_NAME_EMPTY: - case sfp.NODE_TYPE_NET_OPTION_NAME_FONT: - case sfp.NODE_TYPE_NET_OPTION_NAME_FRAME: - case sfp.NODE_TYPE_NET_OPTION_NAME_FROM: - case sfp.NODE_TYPE_NET_OPTION_NAME_GENERICBLOCK: - case sfp.NODE_TYPE_NET_OPTION_NAME_GHIDE: - case sfp.NODE_TYPE_NET_OPTION_NAME_HEADER: - case sfp.NODE_TYPE_NET_OPTION_NAME_IMAGE: - case sfp.NODE_TYPE_NET_OPTION_NAME_IMPORTANT: - case sfp.NODE_TYPE_NET_OPTION_NAME_INLINEFONT: - case sfp.NODE_TYPE_NET_OPTION_NAME_INLINESCRIPT: - case sfp.NODE_TYPE_NET_OPTION_NAME_MATCHCASE: - case sfp.NODE_TYPE_NET_OPTION_NAME_MEDIA: - case sfp.NODE_TYPE_NET_OPTION_NAME_METHOD: - case sfp.NODE_TYPE_NET_OPTION_NAME_MP4: - case sfp.NODE_TYPE_NET_OPTION_NAME_NOOP: - case sfp.NODE_TYPE_NET_OPTION_NAME_OBJECT: - case sfp.NODE_TYPE_NET_OPTION_NAME_OTHER: - case sfp.NODE_TYPE_NET_OPTION_NAME_PING: - case sfp.NODE_TYPE_NET_OPTION_NAME_POPUNDER: - case sfp.NODE_TYPE_NET_OPTION_NAME_POPUP: + return 'variable'; + case sfp.NODE_TYPE_NET_PATTERN_PART: + return 'variable'; + case sfp.NODE_TYPE_NET_PATTERN_PART_SPECIAL: + return 'keyword strong'; + case sfp.NODE_TYPE_NET_PATTERN_PART_UNICODE: + return 'variable unicode'; + case sfp.NODE_TYPE_NET_PATTERN_LEFT_HNANCHOR: + case sfp.NODE_TYPE_NET_PATTERN_LEFT_ANCHOR: + case sfp.NODE_TYPE_NET_PATTERN_RIGHT_ANCHOR: + case sfp.NODE_TYPE_NET_OPTION_NAME_NOT: + return 'keyword strong'; + case sfp.NODE_TYPE_NET_OPTIONS_ANCHOR: + case sfp.NODE_TYPE_NET_OPTION_SEPARATOR: + mode.lastNetOptionType = 0; + return 'def strong'; + case sfp.NODE_TYPE_NET_OPTION_NAME_UNKNOWN: + mode.lastNetOptionType = 0; + return 'error'; + case sfp.NODE_TYPE_NET_OPTION_NAME_1P: + case sfp.NODE_TYPE_NET_OPTION_NAME_STRICT1P: + case sfp.NODE_TYPE_NET_OPTION_NAME_3P: + case sfp.NODE_TYPE_NET_OPTION_NAME_STRICT3P: + case sfp.NODE_TYPE_NET_OPTION_NAME_ALL: + case sfp.NODE_TYPE_NET_OPTION_NAME_BADFILTER: + case sfp.NODE_TYPE_NET_OPTION_NAME_CNAME: + case sfp.NODE_TYPE_NET_OPTION_NAME_CSP: + case sfp.NODE_TYPE_NET_OPTION_NAME_CSS: + case sfp.NODE_TYPE_NET_OPTION_NAME_DENYALLOW: + case sfp.NODE_TYPE_NET_OPTION_NAME_DOC: + case sfp.NODE_TYPE_NET_OPTION_NAME_EHIDE: + case sfp.NODE_TYPE_NET_OPTION_NAME_EMPTY: + case sfp.NODE_TYPE_NET_OPTION_NAME_FONT: + case sfp.NODE_TYPE_NET_OPTION_NAME_FRAME: + case sfp.NODE_TYPE_NET_OPTION_NAME_FROM: + case sfp.NODE_TYPE_NET_OPTION_NAME_GENERICBLOCK: + case sfp.NODE_TYPE_NET_OPTION_NAME_GHIDE: + case sfp.NODE_TYPE_NET_OPTION_NAME_HEADER: + case sfp.NODE_TYPE_NET_OPTION_NAME_IMAGE: + case sfp.NODE_TYPE_NET_OPTION_NAME_IMPORTANT: + case sfp.NODE_TYPE_NET_OPTION_NAME_INLINEFONT: + case sfp.NODE_TYPE_NET_OPTION_NAME_INLINESCRIPT: + case sfp.NODE_TYPE_NET_OPTION_NAME_MATCHCASE: + case sfp.NODE_TYPE_NET_OPTION_NAME_MEDIA: + case sfp.NODE_TYPE_NET_OPTION_NAME_METHOD: + case sfp.NODE_TYPE_NET_OPTION_NAME_MP4: + case sfp.NODE_TYPE_NET_OPTION_NAME_NOOP: + case sfp.NODE_TYPE_NET_OPTION_NAME_OBJECT: + case sfp.NODE_TYPE_NET_OPTION_NAME_OTHER: + case sfp.NODE_TYPE_NET_OPTION_NAME_PING: + case sfp.NODE_TYPE_NET_OPTION_NAME_POPUNDER: + case sfp.NODE_TYPE_NET_OPTION_NAME_POPUP: + case sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECT: + case sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE: + case sfp.NODE_TYPE_NET_OPTION_NAME_REMOVEPARAM: + case sfp.NODE_TYPE_NET_OPTION_NAME_SCRIPT: + case sfp.NODE_TYPE_NET_OPTION_NAME_SHIDE: + case sfp.NODE_TYPE_NET_OPTION_NAME_TO: + case sfp.NODE_TYPE_NET_OPTION_NAME_URLTRANSFORM: + case sfp.NODE_TYPE_NET_OPTION_NAME_XHR: + case sfp.NODE_TYPE_NET_OPTION_NAME_WEBRTC: + case sfp.NODE_TYPE_NET_OPTION_NAME_WEBSOCKET: + mode.lastNetOptionType = nodeType; + return 'def'; + case sfp.NODE_TYPE_NET_OPTION_ASSIGN: + return 'def'; + case sfp.NODE_TYPE_NET_OPTION_VALUE: + if ( mode.astWalker.canGoDown() ) { break; } + switch ( mode.lastNetOptionType ) { case sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECT: case sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE: - case sfp.NODE_TYPE_NET_OPTION_NAME_REMOVEPARAM: - case sfp.NODE_TYPE_NET_OPTION_NAME_SCRIPT: - case sfp.NODE_TYPE_NET_OPTION_NAME_SHIDE: - case sfp.NODE_TYPE_NET_OPTION_NAME_TO: - case sfp.NODE_TYPE_NET_OPTION_NAME_XHR: - case sfp.NODE_TYPE_NET_OPTION_NAME_WEBRTC: - case sfp.NODE_TYPE_NET_OPTION_NAME_WEBSOCKET: - lastNetOptionType = nodeType; - return 'def'; - case sfp.NODE_TYPE_NET_OPTION_ASSIGN: - return 'def'; - case sfp.NODE_TYPE_NET_OPTION_VALUE: - if ( astWalker.canGoDown() ) { break; } - switch ( lastNetOptionType ) { - case sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECT: - case sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE: - return redirectTokenStyle(); - default: - break; - } - return 'value'; - case sfp.NODE_TYPE_OPTION_VALUE_NOT: - return 'keyword strong'; - case sfp.NODE_TYPE_OPTION_VALUE_DOMAIN: - return 'value'; - case sfp.NODE_TYPE_OPTION_VALUE_SEPARATOR: - return 'def'; + return redirectTokenStyle(mode); default: break; + } + return 'value'; + case sfp.NODE_TYPE_OPTION_VALUE_NOT: + return 'keyword strong'; + case sfp.NODE_TYPE_OPTION_VALUE_DOMAIN: + return 'value'; + case sfp.NODE_TYPE_OPTION_VALUE_SEPARATOR: + return 'def'; + default: + break; } return '+'; }; - self.addEventListener('trustedSource', ev => { - astParser.options.trustedSource = ev.detail; - }); - - self.addEventListener('trustedScriptletTokens', ev => { - astParser.options.trustedScriptletTokens = ev.detail; - }); + class ModeState { + constructor() { + this.astParser = new sfp.AstFilterParser({ + interactive: true, + nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'), + }); + this.astWalker = this.astParser.getWalker(); + this.currentWalkerNode = 0; + this.lastNetOptionType = 0; + self.addEventListener('trustedSource', ev => { + const { trusted } = ev.detail; + this.astParser.options.trustedSource = trusted; + }); + self.addEventListener('trustedScriptletTokens', ev => { + this.astParser.options.trustedScriptletTokens = ev.detail; + }); + } + } - return { - lineComment: '!', - token: function(stream) { + return { + state: null, + startState() { + if ( this.state === null ) { + this.state = new ModeState(); + } + return this.state; + }, + copyState(other) { + return other; + }, + token(stream, state) { if ( stream.sol() ) { - astParser.parse(stream.string); - if ( astParser.getFlags(sfp.AST_FLAG_UNSUPPORTED) !== 0 ) { + state.astParser.parse(stream.string); + if ( state.astParser.getFlags(sfp.AST_FLAG_UNSUPPORTED) !== 0 ) { stream.skipToEnd(); return 'error'; } - if ( astParser.getType() === sfp.AST_TYPE_NONE ) { + if ( state.astParser.getType() === sfp.AST_TYPE_NONE ) { stream.skipToEnd(); return 'comment'; } - currentWalkerNode = astWalker.reset(); - } else if ( nodeHasError() ) { - currentWalkerNode = astWalker.right(); + state.currentWalkerNode = state.astWalker.reset(); + } else if ( nodeHasError(state) ) { + state.currentWalkerNode = state.astWalker.right(); } else { - currentWalkerNode = astWalker.next(); + state.currentWalkerNode = state.astWalker.next(); } let style = ''; - while ( currentWalkerNode !== 0 ) { - style = colorFromAstNode(stream); + while ( state.currentWalkerNode !== 0 ) { + style = colorFromAstNode(state, stream); if ( style !== '+' ) { break; } - currentWalkerNode = astWalker.next(); + state.currentWalkerNode = state.astWalker.next(); } if ( style === '+' ) { stream.skipToEnd(); return null; } - stream.pos = astParser.getNodeStringEnd(currentWalkerNode); - if ( astParser.isNetworkFilter() ) { + stream.pos = state.astParser.getNodeStringEnd(state.currentWalkerNode); + if ( state.astParser.isNetworkFilter() ) { return style ? `line-cm-net ${style}` : 'line-cm-net'; } - if ( astParser.isExtendedFilter() ) { + if ( state.astParser.isExtendedFilter() ) { let flavor = ''; - if ( astParser.isCosmeticFilter() ) { + if ( state.astParser.isCosmeticFilter() ) { flavor = 'line-cm-ext-dom'; - } else if ( astParser.isScriptletFilter() ) { + } else if ( state.astParser.isScriptletFilter() ) { flavor = 'line-cm-ext-js'; - } else if ( astParser.isHtmlFilter() ) { + } else if ( state.astParser.isHtmlFilter() ) { flavor = 'line-cm-ext-html'; } if ( flavor !== '' ) { @@ -278,9 +289,11 @@ CodeMirror.defineMode('ubo-static-filtering', function() { style = style.trim(); return style !== '' ? style : null; }, - parser: astParser, + lineComment: '!', }; -}); +})(); + +CodeMirror.defineMode('ubo-static-filtering', ( ) => uBOStaticFilteringMode); /******************************************************************************/ @@ -327,7 +340,7 @@ function initHints() { }); const proceduralOperatorNames = new Map( Array.from(sfp.proceduralOperatorTokens) - .filter(item => (item[1] & 0b01) !== 0) + .filter(item => (item[1] & 0b01) !== 0) ); const excludedHints = new Set([ 'genericblock', @@ -562,7 +575,7 @@ function initHints() { const getExtScriptletHints = function(cursor, line) { const beg = cursor.ch; - const matchLeft = /#\+\js\(([^,]*)$/.exec(line.slice(0, beg)); + const matchLeft = /#\+js\(([^,]*)$/.exec(line.slice(0, beg)); const matchRight = /^([^,)]*)/.exec(line.slice(beg)); if ( matchLeft === null || matchRight === null ) { return; } const hints = []; @@ -709,38 +722,38 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => { if ( astParser.hasError() ) { let msg = 'Invalid filter'; switch ( astParser.astError ) { - case sfp.AST_ERROR_UNSUPPORTED: - msg = `${msg}: Unsupported filter syntax`; - break; - case sfp.AST_ERROR_REGEX: - msg = `${msg}: Bad regular expression`; - break; - case sfp.AST_ERROR_PATTERN: - msg = `${msg}: Bad pattern`; - break; - case sfp.AST_ERROR_DOMAIN_NAME: - msg = `${msg}: Bad domain name`; - break; - case sfp.AST_ERROR_OPTION_BADVALUE: - msg = `${msg}: Bad value assigned to a valid option`; - break; - case sfp.AST_ERROR_OPTION_DUPLICATE: - msg = `${msg}: Duplicate filter option`; - break; - case sfp.AST_ERROR_OPTION_UNKNOWN: - msg = `${msg}: Unsupported filter option`; - break; - case sfp.AST_ERROR_IF_TOKEN_UNKNOWN: - msg = `${msg}: Unknown preparsing token`; - break; - case sfp.AST_ERROR_UNTRUSTED_SOURCE: - msg = `${msg}: Filter requires trusted source`; - break; - default: - if ( astParser.isCosmeticFilter() && astParser.result.error ) { - msg = `${msg}: ${astParser.result.error}`; - } - break; + case sfp.AST_ERROR_UNSUPPORTED: + msg = `${msg}: Unsupported filter syntax`; + break; + case sfp.AST_ERROR_REGEX: + msg = `${msg}: Bad regular expression`; + break; + case sfp.AST_ERROR_PATTERN: + msg = `${msg}: Bad pattern`; + break; + case sfp.AST_ERROR_DOMAIN_NAME: + msg = `${msg}: Bad domain name`; + break; + case sfp.AST_ERROR_OPTION_BADVALUE: + msg = `${msg}: Bad value assigned to a valid option`; + break; + case sfp.AST_ERROR_OPTION_DUPLICATE: + msg = `${msg}: Duplicate filter option`; + break; + case sfp.AST_ERROR_OPTION_UNKNOWN: + msg = `${msg}: Unsupported filter option`; + break; + case sfp.AST_ERROR_IF_TOKEN_UNKNOWN: + msg = `${msg}: Unknown preparsing token`; + break; + case sfp.AST_ERROR_UNTRUSTED_SOURCE: + msg = `${msg}: Filter requires trusted source`; + break; + default: + if ( astParser.isCosmeticFilter() && astParser.result.error ) { + msg = `${msg}: ${astParser.result.error}`; + } + break; } return { lint: 'error', msg }; } @@ -877,6 +890,11 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => { ifendifSet.add(lineHandle); ifendifSetChanged = true; } + } else if ( marker.dataset.lint === 'error' ) { + if ( marker.dataset.error !== 'y' ) { + marker.dataset.error = 'y'; + errorCount += 1; + } } if ( typeof details.msg !== 'string' || details.msg === '' ) { return; } const msgElem = qs$(marker, '.msg'); @@ -1083,7 +1101,8 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => { }; self.addEventListener('trustedSource', ev => { - astParser.options.trustedSource = ev.detail; + const { trusted } = ev.detail; + astParser.options.trustedSource = trusted; }); self.addEventListener('trustedScriptletTokens', ev => { diff --git a/src/js/commands.js b/src/js/commands.js index 8fd6341..2f29b23 100644 --- a/src/js/commands.js +++ b/src/js/commands.js @@ -136,8 +136,11 @@ vAPI.commands.onCommand.addListener(async command => { // Tab-specific commands const tab = await vAPI.tabs.getCurrent(); if ( tab instanceof Object === false ) { return; } + switch ( command ) { case 'launch-element-picker': + if ( µb.userFiltersAreEnabled() === false ) { break; } + /* fall through */ case 'launch-element-zapper': { µb.epickerArgs.mouse = false; µb.elementPickerExec( @@ -168,6 +171,13 @@ vAPI.commands.onCommand.addListener(async command => { hostname: hostnameFromURI(µb.normalizeTabURL(tab.id, tab.url)), }); break; + case 'toggle-javascript': + µb.toggleHostnameSwitch({ + name: 'no-scripting', + hostname: hostnameFromURI(µb.normalizeTabURL(tab.id, tab.url)), + }); + vAPI.tabs.reload(tab.id); + break; default: break; } diff --git a/src/js/contentscript-extra.js b/src/js/contentscript-extra.js index 45c5262..34b0ef0 100644 --- a/src/js/contentscript-extra.js +++ b/src/js/contentscript-extra.js @@ -30,6 +30,9 @@ if ( /******************************************************************************/ const nonVisualElements = { + head: true, + link: true, + meta: true, script: true, style: true, }; @@ -196,28 +199,27 @@ class PSelectorOthersTask extends PSelectorTask { const toKeep = new Set(this.targets); const toDiscard = new Set(); const body = document.body; + const head = document.head; let discard = null; for ( let keep of this.targets ) { - while ( keep !== null && keep !== body ) { + while ( keep !== null && keep !== body && keep !== head ) { toKeep.add(keep); toDiscard.delete(keep); discard = keep.previousElementSibling; while ( discard !== null ) { - if ( - nonVisualElements[discard.localName] !== true && - toKeep.has(discard) === false - ) { - toDiscard.add(discard); + if ( nonVisualElements[discard.localName] !== true ) { + if ( toKeep.has(discard) === false ) { + toDiscard.add(discard); + } } discard = discard.previousElementSibling; } discard = keep.nextElementSibling; while ( discard !== null ) { - if ( - nonVisualElements[discard.localName] !== true && - toKeep.has(discard) === false - ) { - toDiscard.add(discard); + if ( nonVisualElements[discard.localName] !== true ) { + if ( toKeep.has(discard) === false ) { + toDiscard.add(discard); + } } discard = discard.nextElementSibling; } @@ -240,6 +242,36 @@ class PSelectorOthersTask extends PSelectorTask { } } +class PSelectorShadowTask extends PSelectorTask { + constructor(task) { + super(); + this.selector = task[1]; + } + transpose(node, output) { + const root = this.openOrClosedShadowRoot(node); + if ( root === null ) { return; } + const nodes = root.querySelectorAll(this.selector); + output.push(...nodes); + } + get openOrClosedShadowRoot() { + if ( PSelectorShadowTask.openOrClosedShadowRoot !== undefined ) { + return PSelectorShadowTask.openOrClosedShadowRoot; + } + if ( typeof chrome === 'object' && chrome !== null ) { + if ( chrome.dom instanceof Object ) { + if ( typeof chrome.dom.openOrClosedShadowRoot === 'function' ) { + PSelectorShadowTask.openOrClosedShadowRoot = + chrome.dom.openOrClosedShadowRoot; + return PSelectorShadowTask.openOrClosedShadowRoot; + } + } + } + PSelectorShadowTask.openOrClosedShadowRoot = node => + node.openOrClosedShadowRoot || null; + return PSelectorShadowTask.openOrClosedShadowRoot; + } +} + // https://github.com/AdguardTeam/ExtendedCss/issues/31#issuecomment-302391277 // Prepend `:scope ` if needed. class PSelectorSpathTask extends PSelectorTask { @@ -364,7 +396,6 @@ class PSelectorXpathTask extends PSelectorTask { class PSelector { constructor(o) { - this.raw = o.raw; this.selector = o.selector; this.tasks = []; const tasks = []; @@ -435,6 +466,7 @@ PSelector.prototype.operatorToTaskMap = new Map([ [ 'min-text-length', PSelectorMinTextLengthTask ], [ 'not', PSelectorIfNotTask ], [ 'others', PSelectorOthersTask ], + [ 'shadow', PSelectorShadowTask ], [ 'spath', PSelectorSpathTask ], [ 'upward', PSelectorUpwardTask ], [ 'watch-attr', PSelectorWatchAttrs ], diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 8f3a4cf..95dbdb6 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -462,28 +462,6 @@ vAPI.SafeAnimationFrame = class { /******************************************************************************/ /******************************************************************************/ -/******************************************************************************/ - -vAPI.injectScriptlet = function(doc, text) { - if ( !doc ) { return; } - let script, url; - try { - const blob = new self.Blob([ text ], { type: 'text/javascript; charset=utf-8' }); - url = self.URL.createObjectURL(blob); - script = doc.createElement('script'); - script.async = false; - script.src = url; - (doc.head || doc.documentElement || doc).appendChild(script); - } catch (ex) { - } - if ( url ) { - if ( script ) { script.remove(); } - self.URL.revokeObjectURL(url); - } -}; - -/******************************************************************************/ -/******************************************************************************/ /******************************************************************************* The DOM filterer is the heart of uBO's cosmetic filtering. @@ -1298,7 +1276,6 @@ vAPI.DOMFilterer = class { const { noSpecificCosmeticFiltering, noGenericCosmeticFiltering, - scriptletDetails, } = response; vAPI.noSpecificCosmeticFiltering = noSpecificCosmeticFiltering; @@ -1320,14 +1297,6 @@ vAPI.DOMFilterer = class { vAPI.userStylesheet.apply(); } - if ( scriptletDetails && typeof self.uBO_scriptletsInjected !== 'string' ) { - self.uBO_scriptletsInjected = scriptletDetails.filters; - if ( scriptletDetails.mainWorld ) { - vAPI.injectScriptlet(document, scriptletDetails.mainWorld); - vAPI.injectedScripts = scriptletDetails.mainWorld; - } - } - if ( vAPI.domSurveyor ) { if ( Array.isArray(cfeDetails.genericCosmeticHashes) ) { vAPI.domSurveyor.addHashes(cfeDetails.genericCosmeticHashes); diff --git a/src/js/contextmenu.js b/src/js/contextmenu.js index abf0582..788b62b 100644 --- a/src/js/contextmenu.js +++ b/src/js/contextmenu.js @@ -200,7 +200,11 @@ let currentBits = 0; const update = function(tabId = undefined) { let newBits = 0; - if ( µb.userSettings.contextMenuEnabled && tabId !== undefined ) { + if ( + µb.userSettings.contextMenuEnabled && + µb.userFiltersAreEnabled() && + tabId !== undefined + ) { const pageStore = µb.pageStoreFromTabId(tabId); if ( pageStore && pageStore.getNetFilteringSwitch() ) { if ( pageStore.shouldApplySpecificCosmeticFilters(0) ) { diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index f4782bc..9ce1bf4 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -221,7 +221,7 @@ const reEscapeSequence = /\\([0-9A-Fa-f]+ |.)/g; // Generic filters can only be enforced once the main document is loaded. // Specific filers can be enforced before the main document is loaded. -const FilterContainer = function() { +const CosmeticFilteringEngine = function() { this.reSimpleHighGeneric = /^(?:[a-z]*\[[^\]]+\]|\S+)$/; this.selectorCache = new Map(); @@ -269,7 +269,7 @@ const FilterContainer = function() { // Reset all, thus reducing to a minimum memory footprint of the context. -FilterContainer.prototype.reset = function() { +CosmeticFilteringEngine.prototype.reset = function() { this.frozen = false; this.acceptedCount = 0; this.discardedCount = 0; @@ -292,12 +292,12 @@ FilterContainer.prototype.reset = function() { this.highlyGeneric.complex.str = ''; this.highlyGeneric.complex.mru.reset(); - this.selfieVersion = 1; + this.selfieVersion = 2; }; /******************************************************************************/ -FilterContainer.prototype.freeze = function() { +CosmeticFilteringEngine.prototype.freeze = function() { this.duplicateBuster.clear(); this.specificFilters.collectGarbage(); @@ -311,7 +311,7 @@ FilterContainer.prototype.freeze = function() { /******************************************************************************/ -FilterContainer.prototype.compile = function(parser, writer) { +CosmeticFilteringEngine.prototype.compile = function(parser, writer) { if ( parser.hasOptions() === false ) { this.compileGenericSelector(parser, writer); return true; @@ -337,7 +337,7 @@ FilterContainer.prototype.compile = function(parser, writer) { /******************************************************************************/ -FilterContainer.prototype.compileGenericSelector = function(parser, writer) { +CosmeticFilteringEngine.prototype.compileGenericSelector = function(parser, writer) { if ( parser.isException() ) { this.compileGenericUnhideSelector(parser, writer); } else { @@ -347,7 +347,7 @@ FilterContainer.prototype.compileGenericSelector = function(parser, writer) { /******************************************************************************/ -FilterContainer.prototype.compileGenericHideSelector = function( +CosmeticFilteringEngine.prototype.compileGenericHideSelector = function( parser, writer ) { @@ -403,7 +403,7 @@ FilterContainer.prototype.compileGenericHideSelector = function( /******************************************************************************/ -FilterContainer.prototype.compileGenericUnhideSelector = function( +CosmeticFilteringEngine.prototype.compileGenericUnhideSelector = function( parser, writer ) { @@ -432,7 +432,7 @@ FilterContainer.prototype.compileGenericUnhideSelector = function( /******************************************************************************/ -FilterContainer.prototype.compileSpecificSelector = function( +CosmeticFilteringEngine.prototype.compileSpecificSelector = function( parser, hostname, not, @@ -471,7 +471,7 @@ FilterContainer.prototype.compileSpecificSelector = function( /******************************************************************************/ -FilterContainer.prototype.fromCompiledContent = function(reader, options) { +CosmeticFilteringEngine.prototype.fromCompiledContent = function(reader, options) { if ( options.skipCosmetic ) { this.skipCompiledContent(reader, 'SPECIFIC'); this.skipCompiledContent(reader, 'GENERIC'); @@ -560,7 +560,7 @@ FilterContainer.prototype.fromCompiledContent = function(reader, options) { /******************************************************************************/ -FilterContainer.prototype.skipCompiledContent = function(reader, sectionId) { +CosmeticFilteringEngine.prototype.skipCompiledContent = function(reader, sectionId) { reader.select(`COSMETIC_FILTERS:${sectionId}`); while ( reader.next() ) { this.acceptedCount += 1; @@ -570,21 +570,23 @@ FilterContainer.prototype.skipCompiledContent = function(reader, sectionId) { /******************************************************************************/ -FilterContainer.prototype.toSelfie = function() { +CosmeticFilteringEngine.prototype.toSelfie = function() { return { version: this.selfieVersion, acceptedCount: this.acceptedCount, discardedCount: this.discardedCount, specificFilters: this.specificFilters.toSelfie(), - lowlyGeneric: Array.from(this.lowlyGeneric), - highSimpleGenericHideArray: Array.from(this.highlyGeneric.simple.dict), - highComplexGenericHideArray: Array.from(this.highlyGeneric.complex.dict), + lowlyGeneric: this.lowlyGeneric, + highSimpleGenericHideDict: this.highlyGeneric.simple.dict, + highSimpleGenericHideStr: this.highlyGeneric.simple.str, + highComplexGenericHideDict: this.highlyGeneric.complex.dict, + highComplexGenericHideStr: this.highlyGeneric.complex.str, }; }; /******************************************************************************/ -FilterContainer.prototype.fromSelfie = function(selfie) { +CosmeticFilteringEngine.prototype.fromSelfie = function(selfie) { if ( selfie.version !== this.selfieVersion ) { throw new Error( `cosmeticFilteringEngine: mismatched selfie version, ${selfie.version}, expected ${this.selfieVersion}` @@ -593,17 +595,17 @@ FilterContainer.prototype.fromSelfie = function(selfie) { this.acceptedCount = selfie.acceptedCount; this.discardedCount = selfie.discardedCount; this.specificFilters.fromSelfie(selfie.specificFilters); - this.lowlyGeneric = new Map(selfie.lowlyGeneric); - this.highlyGeneric.simple.dict = new Set(selfie.highSimpleGenericHideArray); - this.highlyGeneric.simple.str = selfie.highSimpleGenericHideArray.join(',\n'); - this.highlyGeneric.complex.dict = new Set(selfie.highComplexGenericHideArray); - this.highlyGeneric.complex.str = selfie.highComplexGenericHideArray.join(',\n'); + this.lowlyGeneric = selfie.lowlyGeneric; + this.highlyGeneric.simple.dict = selfie.highSimpleGenericHideDict; + this.highlyGeneric.simple.str = selfie.highSimpleGenericHideStr; + this.highlyGeneric.complex.dict = selfie.highComplexGenericHideDict; + this.highlyGeneric.complex.str = selfie.highComplexGenericHideStr; this.frozen = true; }; /******************************************************************************/ -FilterContainer.prototype.addToSelectorCache = function(details) { +CosmeticFilteringEngine.prototype.addToSelectorCache = function(details) { const hostname = details.hostname; if ( typeof hostname !== 'string' || hostname === '' ) { return; } const selectors = details.selectors; @@ -621,7 +623,7 @@ FilterContainer.prototype.addToSelectorCache = function(details) { /******************************************************************************/ -FilterContainer.prototype.removeFromSelectorCache = function( +CosmeticFilteringEngine.prototype.removeFromSelectorCache = function( targetHostname = '*', type = undefined ) { @@ -644,7 +646,7 @@ FilterContainer.prototype.removeFromSelectorCache = function( /******************************************************************************/ -FilterContainer.prototype.pruneSelectorCacheAsync = function() { +CosmeticFilteringEngine.prototype.pruneSelectorCacheAsync = function() { if ( this.selectorCache.size <= this.selectorCacheCountMax ) { return; } const cache = this.selectorCache; const hostnames = Array.from(cache.keys()) @@ -658,7 +660,7 @@ FilterContainer.prototype.pruneSelectorCacheAsync = function() { /******************************************************************************/ -FilterContainer.prototype.disableSurveyor = function(details) { +CosmeticFilteringEngine.prototype.disableSurveyor = function(details) { const hostname = details.hostname; if ( typeof hostname !== 'string' || hostname === '' ) { return; } const cacheEntry = this.selectorCache.get(hostname); @@ -668,7 +670,7 @@ FilterContainer.prototype.disableSurveyor = function(details) { /******************************************************************************/ -FilterContainer.prototype.cssRuleFromProcedural = function(pfilter) { +CosmeticFilteringEngine.prototype.cssRuleFromProcedural = function(pfilter) { if ( pfilter.cssable !== true ) { return; } const { tasks, action } = pfilter; let mq, selector; @@ -699,7 +701,7 @@ FilterContainer.prototype.cssRuleFromProcedural = function(pfilter) { /******************************************************************************/ -FilterContainer.prototype.retrieveGenericSelectors = function(request) { +CosmeticFilteringEngine.prototype.retrieveGenericSelectors = function(request) { if ( this.lowlyGeneric.size === 0 ) { return; } if ( Array.isArray(request.hashes) === false ) { return; } if ( request.hashes.length === 0 ) { return; } @@ -757,7 +759,7 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) { /******************************************************************************/ -FilterContainer.prototype.retrieveSpecificSelectors = function( +CosmeticFilteringEngine.prototype.retrieveSpecificSelectors = function( request, options ) { @@ -928,7 +930,7 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( if ( injectedCSS.length !== 0 ) { out.injectedCSS = injectedCSS.join('\n\n'); details.code = out.injectedCSS; - if ( request.tabId !== undefined ) { + if ( request.tabId !== undefined && options.dontInject !== true ) { vAPI.tabs.insertCSS(request.tabId, details); } } @@ -938,7 +940,7 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( const networkFilters = []; if ( cacheEntry.retrieveNet(networkFilters) ) { details.code = `${networkFilters.join('\n')}\n{display:none!important;}`; - if ( request.tabId !== undefined ) { + if ( request.tabId !== undefined && options.dontInject !== true ) { vAPI.tabs.insertCSS(request.tabId, details); } } @@ -949,13 +951,13 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( /******************************************************************************/ -FilterContainer.prototype.getFilterCount = function() { +CosmeticFilteringEngine.prototype.getFilterCount = function() { return this.acceptedCount - this.discardedCount; }; /******************************************************************************/ -FilterContainer.prototype.dump = function() { +CosmeticFilteringEngine.prototype.dump = function() { const lowlyGenerics = []; for ( const selectors of this.lowlyGeneric.values() ) { lowlyGenerics.push(...selectors.split(',\n')); @@ -976,7 +978,7 @@ FilterContainer.prototype.dump = function() { /******************************************************************************/ -const cosmeticFilteringEngine = new FilterContainer(); +const cosmeticFilteringEngine = new CosmeticFilteringEngine(); export default cosmeticFilteringEngine; diff --git a/src/js/dashboard.js b/src/js/dashboard.js index e82ec28..3ba16f0 100644 --- a/src/js/dashboard.js +++ b/src/js/dashboard.js @@ -25,7 +25,7 @@ import { dom, qs$ } from './dom.js'; /******************************************************************************/ -const discardUnsavedData = function(synchronous = false) { +function discardUnsavedData(synchronous = false) { const paneFrame = qs$('#iframe'); const paneWindow = paneFrame.contentWindow; if ( @@ -66,9 +66,9 @@ const discardUnsavedData = function(synchronous = false) { dom.on(document, 'click', onClick, true); }); -}; +} -const loadDashboardPanel = function(pane, first) { +function loadDashboardPanel(pane, first) { const tabButton = qs$(`[data-pane="${pane}"]`); if ( tabButton === null || dom.cl.has(tabButton, 'selected') ) { return; } const loadPane = ( ) => { @@ -76,8 +76,12 @@ const loadDashboardPanel = function(pane, first) { dom.cl.remove('.tabButton.selected', 'selected'); dom.cl.add(tabButton, 'selected'); tabButton.scrollIntoView(); - qs$('#iframe').contentWindow.location.replace(pane); + const iframe = qs$('#iframe'); + iframe.contentWindow.location.replace(pane); if ( pane !== 'no-dashboard.html' ) { + iframe.addEventListener('load', ( ) => { + qs$('.wikilink').href = iframe.contentWindow.wikilink || ''; + }, { once: true }); vAPI.localStorage.setItem('dashboardLastVisitedPane', pane); } }; @@ -91,11 +95,11 @@ const loadDashboardPanel = function(pane, first) { if ( status === false ) { return; } loadPane(); }); -}; +} -const onTabClickHandler = function(ev) { +function onTabClickHandler(ev) { loadDashboardPanel(dom.attr(ev.target, 'data-pane')); -}; +} if ( self.location.hash.slice(1) === 'no-dashboard.html' ) { dom.cl.add(dom.body, 'noDashboard'); diff --git a/src/js/devtools.js b/src/js/devtools.js index 93b2697..0763b0b 100644 --- a/src/js/devtools.js +++ b/src/js/devtools.js @@ -187,6 +187,28 @@ vAPI.messaging.send('dashboard', { dom.attr(button, 'disabled', null); }); }); + dom.attr('#cfe-benchmark', 'disabled', null); + dom.on('#cfe-benchmark', 'click', ev => { + const button = ev.target; + dom.attr(button, 'disabled', ''); + vAPI.messaging.send('devTools', { + what: 'cfeBenchmark', + }).then(result => { + log(result); + dom.attr(button, 'disabled', null); + }); + }); + dom.attr('#sfe-benchmark', 'disabled', null); + dom.on('#sfe-benchmark', 'click', ev => { + const button = ev.target; + dom.attr(button, 'disabled', ''); + vAPI.messaging.send('devTools', { + what: 'sfeBenchmark', + }).then(result => { + log(result); + dom.attr(button, 'disabled', null); + }); + }); }); /******************************************************************************/ diff --git a/src/js/dom.js b/src/js/dom.js index 3d2f517..5c4d194 100644 --- a/src/js/dom.js +++ b/src/js/dom.js @@ -161,9 +161,9 @@ dom.cl = class { } } - static remove(target, name) { + static remove(target, ...names) { for ( const elem of normalizeTarget(target) ) { - elem.classList.remove(name); + elem.classList.remove(...names); } } diff --git a/src/js/dyna-rules.js b/src/js/dyna-rules.js index ea79742..69eef85 100644 --- a/src/js/dyna-rules.js +++ b/src/js/dyna-rules.js @@ -69,7 +69,6 @@ const thePanes = { let cleanEditToken = 0; let cleanEditText = ''; -let isCollapsed = false; /******************************************************************************/ @@ -104,7 +103,6 @@ let isCollapsed = false; qs$('.CodeMirror-merge-copybuttons-left'), { attributes: true, attributeFilter: [ 'title' ], subtree: true } ); - } /******************************************************************************/ @@ -142,21 +140,41 @@ const updateOverlay = (( ) => { stream.skipToEnd(); } }; - return function(filter) { - reFilter = typeof filter === 'string' && filter !== '' ? - new RegExp(filter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi') : - undefined; + return function() { + const f = presentationState.filter; + reFilter = typeof f === 'string' && f !== '' + ? new RegExp(f.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi') + : undefined; return mode; }; })(); +const toggleOverlay = (( ) => { + let overlay = null; + + return function() { + if ( overlay !== null ) { + mergeView.leftOriginal().removeOverlay(overlay); + mergeView.editor().removeOverlay(overlay); + overlay = null; + } + if ( presentationState.filter !== '' ) { + overlay = updateOverlay(); + mergeView.leftOriginal().addOverlay(overlay); + mergeView.editor().addOverlay(overlay); + } + rulesToDoc(true); + savePresentationState(); + }; +})(); + /******************************************************************************/ // Incrementally update text in a CodeMirror editor for best user experience: // - Scroll position preserved // - Minimum amount of text updated -const rulesToDoc = function(clearHistory) { +function rulesToDoc(clearHistory) { const orig = thePanes.orig.doc; const edit = thePanes.edit.doc; orig.startOperation(); @@ -210,7 +228,7 @@ const rulesToDoc = function(clearHistory) { if ( mark.uboEllipsis !== true ) { continue; } mark.clear(); } - if ( isCollapsed ) { + if ( presentationState.isCollapsed ) { for ( let iline = 0, n = edit.lineCount(); iline < n; iline++ ) { if ( edit.getLine(iline) !== '...' ) { continue; } const mark = edit.markText( @@ -240,11 +258,11 @@ const rulesToDoc = function(clearHistory) { { line, ch: 0 }, (clientHeight - ldoc.defaultTextHeight()) / 2 ); -}; +} /******************************************************************************/ -const filterRules = function(key) { +function filterRules(key) { const filter = qs$('#ruleFilter input').value; const rules = thePanes[key].modified; if ( filter === '' ) { return rules; } @@ -254,11 +272,11 @@ const filterRules = function(key) { out.push(rule); } return out; -}; +} /******************************************************************************/ -const applyDiff = async function(permanent, toAdd, toRemove) { +async function applyDiff(permanent, toAdd, toRemove) { const details = await vAPI.messaging.send('dashboard', { what: 'modifyRuleset', permanent: permanent, @@ -268,7 +286,7 @@ const applyDiff = async function(permanent, toAdd, toRemove) { thePanes.orig.original = details.permanentRules; thePanes.edit.original = details.sessionRules; onPresentationChanged(); -}; +} /******************************************************************************/ @@ -327,14 +345,14 @@ function handleImportFilePicker() { /******************************************************************************/ -const startImportFilePicker = function() { +function startImportFilePicker() { const input = qs$('#importFilePicker'); // Reset to empty string, this will ensure an change event is properly // triggered if the user pick a file, even if it is the same as the last // one picked. input.value = ''; input.click(); -}; +} /******************************************************************************/ @@ -353,41 +371,25 @@ function exportUserRulesToFile() { /******************************************************************************/ -const onFilterChanged = (( ) => { +{ let timer; - let overlay = null; - let last = ''; - const process = function() { - timer = undefined; - if ( mergeView.editor().isClean(cleanEditToken) === false ) { return; } - const filter = qs$('#ruleFilter input').value; - if ( filter === last ) { return; } - last = filter; - if ( overlay !== null ) { - mergeView.leftOriginal().removeOverlay(overlay); - mergeView.editor().removeOverlay(overlay); - overlay = null; - } - if ( filter !== '' ) { - overlay = updateOverlay(filter); - mergeView.leftOriginal().addOverlay(overlay); - mergeView.editor().addOverlay(overlay); - } - rulesToDoc(true); - }; - - return function() { + dom.on('#ruleFilter input', 'input', ( ) => { if ( timer !== undefined ) { self.cancelIdleCallback(timer); } - timer = self.requestIdleCallback(process, { timeout: 773 }); - }; -})(); + timer = self.requestIdleCallback(( ) => { + timer = undefined; + if ( mergeView.editor().isClean(cleanEditToken) === false ) { return; } + const filter = qs$('#ruleFilter input').value; + if ( filter === presentationState.filter ) { return; } + presentationState.filter = filter; + toggleOverlay(); + }, { timeout: 773 }); + }); +} /******************************************************************************/ const onPresentationChanged = (( ) => { - let sortType = 1; - const reSwRule = /^([^/]+): ([^/ ]+) ([^ ]+)/; const reRule = /^([^ ]+) ([^/ ]+) ([^ ]+ [^ ]+)/; const reUrlRule = /^([^ ]+) ([^ ]+) ([^ ]+ [^ ]+)/; @@ -431,10 +433,10 @@ const onPresentationChanged = (( ) => { desHn = sortNormalizeHn(hostnameFromURI(match[2])); extra = match[3]; } - if ( sortType === 0 ) { + if ( presentationState.sortType === 0 ) { return { rule, token: `${type} ${srcHn} ${desHn} ${extra}` }; } - if ( sortType === 1 ) { + if ( presentationState.sortType === 1 ) { return { rule, token: `${srcHn} ${type} ${desHn} ${extra}` }; } return { rule, token: `${desHn} ${type} ${srcHn} ${extra}` }; @@ -452,7 +454,7 @@ const onPresentationChanged = (( ) => { }; const collapse = ( ) => { - if ( isCollapsed !== true ) { return; } + if ( presentationState.isCollapsed !== true ) { return; } const diffs = getDiffer().diff_main( thePanes.orig.modified.join('\n'), thePanes.edit.modified.join('\n') @@ -491,23 +493,31 @@ const onPresentationChanged = (( ) => { thePanes.edit.modified = rr; }; - return function(clearHistory) { + dom.on('#ruleFilter select', 'input', ev => { + presentationState.sortType = parseInt(ev.target.value, 10) || 0; + savePresentationState(); + onPresentationChanged(true); + }); + dom.on('#ruleFilter #diffCollapse', 'click', ev => { + presentationState.isCollapsed = dom.cl.toggle(ev.target, 'active'); + savePresentationState(); + onPresentationChanged(true); + }); + + return function onPresentationChanged(clearHistory) { const origPane = thePanes.orig; const editPane = thePanes.edit; origPane.modified = origPane.original.slice(); editPane.modified = editPane.original.slice(); - const select = qs$('#ruleFilter select'); - sortType = parseInt(select.value, 10); - if ( isNaN(sortType) ) { sortType = 1; } { const mode = origPane.doc.getMode(); - mode.sortType = sortType; + mode.sortType = presentationState.sortType; mode.setHostnameToDomainMap(hostnameToDomainMap); mode.setPSL(publicSuffixList); } { const mode = editPane.doc.getMode(); - mode.sortType = sortType; + mode.sortType = presentationState.sortType; mode.setHostnameToDomainMap(hostnameToDomainMap); mode.setPSL(publicSuffixList); } @@ -552,7 +562,7 @@ const onTextChanged = (( ) => { } }; - return function(now) { + return function onTextChanged(now) { if ( timer !== undefined ) { self.cancelIdleCallback(timer); } timer = now ? process() : self.requestIdleCallback(process, { timeout: 57 }); }; @@ -560,7 +570,7 @@ const onTextChanged = (( ) => { /******************************************************************************/ -const revertAllHandler = function() { +function revertAllHandler() { const toAdd = [], toRemove = []; const left = mergeView.leftOriginal(); const edit = mergeView.editor(); @@ -577,11 +587,11 @@ const revertAllHandler = function() { toRemove.push(removedLines.trim()); } applyDiff(false, toAdd.join('\n'), toRemove.join('\n')); -}; +} /******************************************************************************/ -const commitAllHandler = function() { +function commitAllHandler() { const toAdd = [], toRemove = []; const left = mergeView.leftOriginal(); const edit = mergeView.editor(); @@ -598,11 +608,11 @@ const commitAllHandler = function() { toRemove.push(removedLines.trim()); } applyDiff(true, toAdd.join('\n'), toRemove.join('\n')); -}; +} /******************************************************************************/ -const editSaveHandler = function() { +function editSaveHandler() { const editor = mergeView.editor(); const editText = editor.getValue().trim(); if ( editText === cleanEditText ) { @@ -619,7 +629,7 @@ const editSaveHandler = function() { } } applyDiff(false, toAdd.join(''), toRemove.join('')); -}; +} /******************************************************************************/ @@ -638,12 +648,43 @@ self.cloud.onPull = function(data, append) { /******************************************************************************/ +self.wikilink = 'https://github.com/gorhill/uBlock/wiki/Dashboard:-My-rules'; + self.hasUnsavedData = function() { return mergeView.editor().isClean(cleanEditToken) === false; }; /******************************************************************************/ +const presentationState = { + sortType: 0, + isCollapsed: false, + filter: '', +}; + +const savePresentationState = ( ) => { + vAPI.localStorage.setItem('dynaRulesPresentationState', presentationState); +}; + +vAPI.localStorage.getItemAsync('dynaRulesPresentationState').then(details => { + if ( details instanceof Object === false ) { return; } + if ( typeof details.sortType === 'number' ) { + presentationState.sortType = details.sortType; + qs$('#ruleFilter select').value = `${details.sortType}`; + } + if ( typeof details.isCollapsed === 'boolean' ) { + presentationState.isCollapsed = details.isCollapsed; + dom.cl.toggle('#ruleFilter #diffCollapse', 'active', details.isCollapsed); + } + if ( typeof details.filter === 'string' ) { + presentationState.filter = details.filter; + qs$('#ruleFilter input').value = details.filter; + toggleOverlay(); + } +}); + +/******************************************************************************/ + vAPI.messaging.send('dashboard', { what: 'getRules', }).then(details => { @@ -660,14 +701,6 @@ dom.on('#exportButton', 'click', exportUserRulesToFile); dom.on('#revertButton', 'click', revertAllHandler); dom.on('#commitButton', 'click', commitAllHandler); dom.on('#editSaveButton', 'click', editSaveHandler); -dom.on('#ruleFilter input', 'input', onFilterChanged); -dom.on('#ruleFilter select', 'input', ( ) => { - onPresentationChanged(true); -}); -dom.on('#ruleFilter #diffCollapse', 'click', ev => { - isCollapsed = dom.cl.toggle(ev.target, 'active'); - onPresentationChanged(true); -}); // https://groups.google.com/forum/#!topic/codemirror/UQkTrt078Vs mergeView.editor().on('updateDiff', ( ) => { @@ -675,4 +708,3 @@ mergeView.editor().on('updateDiff', ( ) => { }); /******************************************************************************/ - diff --git a/src/js/epicker-ui.js b/src/js/epicker-ui.js index 49fc116..0c7ea1f 100644 --- a/src/js/epicker-ui.js +++ b/src/js/epicker-ui.js @@ -28,6 +28,7 @@ import './codemirror/ubo-static-filtering.js'; import { hostnameFromURI } from './uri-utils.js'; import punycode from '../lib/punycode.js'; import * as sfp from './static-filtering-parser.js'; +import { dom } from './dom.js'; /******************************************************************************/ /******************************************************************************/ @@ -46,7 +47,7 @@ const pickerRoot = document.documentElement; const dialog = $stor('aside'); let staticFilteringParser; -const svgRoot = $stor('svg'); +const svgRoot = $stor('svg#sea'); const svgOcean = svgRoot.children[0]; const svgIslands = svgRoot.children[1]; const NoPaths = 'M0 0'; @@ -594,8 +595,9 @@ const onStartMoving = (( ) => { let isTouch = false; let mx0 = 0, my0 = 0; let mx1 = 0, my1 = 0; - let r0 = 0, b0 = 0; - let rMax = 0, bMax = 0; + let pw = 0, ph = 0; + let dw = 0, dh = 0; + let cx0 = 0, cy0 = 0; let timer; const eatEvent = function(ev) { @@ -605,10 +607,22 @@ const onStartMoving = (( ) => { const move = ( ) => { timer = undefined; - const r1 = Math.min(Math.max(r0 - mx1 + mx0, 2), rMax); - const b1 = Math.min(Math.max(b0 - my1 + my0, 2), bMax); - dialog.style.setProperty('right', `${r1}px`); - dialog.style.setProperty('bottom', `${b1}px`); + const cx1 = cx0 + mx1 - mx0; + const cy1 = cy0 + my1 - my0; + if ( cx1 < pw / 2 ) { + dialog.style.setProperty('left', `${Math.max(cx1-dw/2,2)}px`); + dialog.style.removeProperty('right'); + } else { + dialog.style.removeProperty('left'); + dialog.style.setProperty('right', `${Math.max(pw-cx1-dw/2,2)}px`); + } + if ( cy1 < ph / 2 ) { + dialog.style.setProperty('top', `${Math.max(cy1-dh/2,2)}px`); + dialog.style.removeProperty('bottom'); + } else { + dialog.style.removeProperty('top'); + dialog.style.setProperty('bottom', `${Math.max(ph-cy1-dh/2,2)}px`); + } }; const moveAsync = ev => { @@ -635,7 +649,7 @@ const onStartMoving = (( ) => { eatEvent(ev); }; - return function(ev) { + return ev => { const target = dialog.querySelector('#move'); if ( ev.target !== target ) { return; } if ( dialog.classList.contains('moving') ) { return; } @@ -648,12 +662,13 @@ const onStartMoving = (( ) => { mx0 = ev.pageX; my0 = ev.pageY; } - const style = self.getComputedStyle(dialog); - r0 = parseInt(style.right, 10); - b0 = parseInt(style.bottom, 10); const rect = dialog.getBoundingClientRect(); - rMax = pickerRoot.clientWidth - 2 - rect.width ; - bMax = pickerRoot.clientHeight - 2 - rect.height; + dw = rect.width; + dh = rect.height; + cx0 = rect.x + dw / 2; + cy0 = rect.y + dh / 2; + pw = pickerRoot.clientWidth; + ph = pickerRoot.clientHeight; dialog.classList.add('moving'); if ( isTouch ) { self.addEventListener('touchmove', moveAsync, { capture: true }); @@ -787,14 +802,16 @@ const showDialog = function(details) { /******************************************************************************/ const pausePicker = function() { - pickerRoot.classList.add('paused'); + dom.cl.add(pickerRoot, 'paused'); + dom.cl.remove(pickerRoot, 'minimized'); svgListening(false); }; /******************************************************************************/ const unpausePicker = function() { - pickerRoot.classList.remove('paused', 'preview'); + dom.cl.remove(pickerRoot, 'paused', 'preview'); + dom.cl.add(pickerRoot, 'minimized'); pickerContentPort.postMessage({ what: 'togglePreview', state: false, @@ -806,7 +823,7 @@ const unpausePicker = function() { const startPicker = function() { self.addEventListener('keydown', onKeyPressed, true); - const svg = $stor('svg'); + const svg = $stor('svg#sea'); svg.addEventListener('click', onSvgClicked); svg.addEventListener('touchstart', onSvgTouch); svg.addEventListener('touchend', onSvgTouch); @@ -820,6 +837,14 @@ const startPicker = function() { $id('preview').addEventListener('click', onPreviewClicked); $id('create').addEventListener('click', onCreateClicked); $id('pick').addEventListener('click', onPickClicked); + $id('minimize').addEventListener('click', ( ) => { + if ( dom.cl.has(pickerRoot, 'paused') === false ) { + pausePicker(); + onCandidateChanged(); + } else { + dom.cl.toggle(pickerRoot, 'minimized'); + } + }); $id('quit').addEventListener('click', onQuitClicked); $id('move').addEventListener('mousedown', onStartMoving); $id('move').addEventListener('touchstart', onStartMoving); diff --git a/src/js/fa-icons.js b/src/js/fa-icons.js index 79968d0..5c249b9 100644 --- a/src/js/fa-icons.js +++ b/src/js/fa-icons.js @@ -32,6 +32,7 @@ export const faIconsInit = (( ) => { [ 'arrow-right', { viewBox: '0 0 1472 1558', path: 'm 1472,779 q 0,54 -37,91 l -651,651 q -39,37 -91,37 -51,0 -90,-37 l -75,-75 q -38,-38 -38,-91 0,-53 38,-91 L 821,971 H 117 Q 65,971 32.5,933.5 0,896 0,843 V 715 Q 0,662 32.5,624.5 65,587 117,587 H 821 L 528,293 q -38,-36 -38,-90 0,-54 38,-90 l 75,-75 q 38,-38 90,-38 53,0 91,38 l 651,651 q 37,35 37,90 z' } ], [ 'bar-chart', { viewBox: '0 0 2048 1536', path: 'm 640,768 0,512 -256,0 0,-512 256,0 z m 384,-512 0,1024 -256,0 0,-1024 256,0 z m 1024,1152 0,128 L 0,1536 0,0 l 128,0 0,1408 1920,0 z m -640,-896 0,768 -256,0 0,-768 256,0 z m 384,-384 0,1152 -256,0 0,-1152 256,0 z' } ], [ 'bolt', { viewBox: '0 0 896 1664', path: 'm 885.08696,438 q 18,20 7,44 l -540,1157 q -13,25 -42,25 -4,0 -14,-2 -17,-5 -25.5,-19 -8.5,-14 -4.5,-30 l 197,-808 -406,101 q -4,1 -12,1 -18,0 -31,-11 Q -3.9130435,881 1.0869565,857 L 202.08696,32 q 4,-14 16,-23 12,-9 28,-9 l 328,0 q 19,0 32,12.5 13,12.5 13,29.5 0,8 -5,18 l -171,463 396,-98 q 8,-2 12,-2 19,0 34,15 z' } ], + [ 'book', { viewBox: '0 0 1664 1536', path: 'm 1639.2625,350 c 25,36 32,83 18,129 l -275,906 c -25,85 -113,151 -199,151 H 260.26251 c -102,0 -211,-81 -248,-185 -16,-45 -16,-89 -2,-127 2,-20 6,-40 7,-64 1,-16 -8,-29 -6,-41 4,-24 25,-41 41,-68 30,-50 64,-131 75,-183 5,-19 -5,-41 0,-58 5,-19 24,-33 34,-51 27,-46 62,-135 67,-182 2,-21 -8,-44 -2,-60 7,-23 29,-33 44,-53 24,-33 64,-128 70,-181 2,-17 -8,-34 -5,-52 4,-19 28,-39 44,-62 42,-62 50,-199 177,-163 l -1,3 c 17,-4 34,-9 51,-9 h 761 c 47,0 89,21 114,56 26,36 32,83 18,130 l -274,906 c -47,154 -73,188 -200,188 H 156.26251 c -13,0 -29,3 -38,15 -8,12 -9,21 -1,43 20,58 89,70 144,70 h 923 c 37,0 80,-21 91,-57 l 300,-987 c 6,-19 6,-39 5,-57 23,9 44,23 59,43 z m -1064,2 c -6,18 4,32 22,32 h 608 c 17,0 36,-14 42,-32 l 21,-64 c 6,-18 -4,-32 -22,-32 H 638.26251 c -17,0 -36,14 -42,32 z m -83,256 c -6,18 4,32 22,32 h 608 c 17,0 36,-14 42,-32 l 21,-64 c 6,-18 -4,-32 -22,-32 H 555.26251 c -17,0 -36,14 -42,32 z' } ], [ 'clipboard', { viewBox: '0 0 1792 1792', path: 'm 768,1664 896,0 0,-640 -416,0 q -40,0 -68,-28 -28,-28 -28,-68 l 0,-416 -384,0 0,1152 z m 256,-1440 0,-64 q 0,-13 -9.5,-22.5 Q 1005,128 992,128 l -704,0 q -13,0 -22.5,9.5 Q 256,147 256,160 l 0,64 q 0,13 9.5,22.5 9.5,9.5 22.5,9.5 l 704,0 q 13,0 22.5,-9.5 9.5,-9.5 9.5,-22.5 z m 256,672 299,0 -299,-299 0,299 z m 512,128 0,672 q 0,40 -28,68 -28,28 -68,28 l -960,0 q -40,0 -68,-28 -28,-28 -28,-68 l 0,-160 -544,0 Q 56,1536 28,1508 0,1480 0,1440 L 0,96 Q 0,56 28,28 56,0 96,0 l 1088,0 q 40,0 68,28 28,28 28,68 l 0,328 q 21,13 36,28 l 408,408 q 28,28 48,76 20,48 20,88 z' } ], [ 'clock-o', { viewBox: '0 0 1536 1536', path: 'm 896,416 v 448 q 0,14 -9,23 -9,9 -23,9 H 544 q -14,0 -23,-9 -9,-9 -9,-23 v -64 q 0,-14 9,-23 9,-9 23,-9 H 768 V 416 q 0,-14 9,-23 9,-9 23,-9 h 64 q 14,0 23,9 9,9 9,23 z m 416,352 q 0,-148 -73,-273 -73,-125 -198,-198 -125,-73 -273,-73 -148,0 -273,73 -125,73 -198,198 -73,125 -73,273 0,148 73,273 73,125 198,198 125,73 273,73 148,0 273,-73 125,-73 198,-198 73,-125 73,-273 z m 224,0 q 0,209 -103,385.5 Q 1330,1330 1153.5,1433 977,1536 768,1536 559,1536 382.5,1433 206,1330 103,1153.5 0,977 0,768 0,559 103,382.5 206,206 382.5,103 559,0 768,0 977,0 1153.5,103 1330,206 1433,382.5 1536,559 1536,768 Z' } ], [ 'cloud-download', { viewBox: '0 0 1920 1408', path: 'm 1280,800 q 0,-14 -9,-23 -9,-9 -23,-9 l -224,0 0,-352 q 0,-13 -9.5,-22.5 Q 1005,384 992,384 l -192,0 q -13,0 -22.5,9.5 Q 768,403 768,416 l 0,352 -224,0 q -13,0 -22.5,9.5 -9.5,9.5 -9.5,22.5 0,14 9,23 l 352,352 q 9,9 23,9 14,0 23,-9 l 351,-351 q 10,-12 10,-24 z m 640,224 q 0,159 -112.5,271.5 Q 1695,1408 1536,1408 l -1088,0 Q 263,1408 131.5,1276.5 0,1145 0,960 0,830 70,720 140,610 258,555 256,525 256,512 256,300 406,150 556,0 768,0 q 156,0 285.5,87 129.5,87 188.5,231 71,-62 166,-62 106,0 181,75 75,75 75,181 0,76 -41,138 130,31 213.5,135.5 Q 1920,890 1920,1024 Z' } ], @@ -78,6 +79,7 @@ export const faIconsInit = (( ) => { [ 'unlink', { viewBox: '0 0 1664 1664', path: 'm 439,1271 -256,256 q -11,9 -23,9 -12,0 -23,-9 -9,-10 -9,-23 0,-13 9,-23 l 256,-256 q 10,-9 23,-9 13,0 23,9 9,10 9,23 0,13 -9,23 z m 169,41 v 320 q 0,14 -9,23 -9,9 -23,9 -14,0 -23,-9 -9,-9 -9,-23 v -320 q 0,-14 9,-23 9,-9 23,-9 14,0 23,9 9,9 9,23 z M 384,1088 q 0,14 -9,23 -9,9 -23,9 H 32 q -14,0 -23,-9 -9,-9 -9,-23 0,-14 9,-23 9,-9 23,-9 h 320 q 14,0 23,9 9,9 9,23 z m 1264,128 q 0,120 -85,203 l -147,146 q -83,83 -203,83 -121,0 -204,-85 L 675,1228 q -21,-21 -42,-56 l 239,-18 273,274 q 27,27 68,27.5 41,0.5 68,-26.5 l 147,-146 q 28,-28 28,-67 0,-40 -28,-68 l -274,-275 18,-239 q 35,21 56,42 l 336,336 q 84,86 84,204 z M 1031,492 792,510 519,236 q -28,-28 -68,-28 -39,0 -68,27 L 236,381 q -28,28 -28,67 0,40 28,68 l 274,274 -18,240 q -35,-21 -56,-42 L 100,652 Q 16,566 16,448 16,328 101,245 L 248,99 q 83,-83 203,-83 121,0 204,85 l 334,335 q 21,21 42,56 z m 633,84 q 0,14 -9,23 -9,9 -23,9 h -320 q -14,0 -23,-9 -9,-9 -9,-23 0,-14 9,-23 9,-9 23,-9 h 320 q 14,0 23,9 9,9 9,23 z M 1120,32 v 320 q 0,14 -9,23 -9,9 -23,9 -14,0 -23,-9 -9,-9 -9,-23 V 32 q 0,-14 9,-23 9,-9 23,-9 14,0 23,9 9,9 9,23 z m 407,151 -256,256 q -11,9 -23,9 -12,0 -23,-9 -9,-10 -9,-23 0,-13 9,-23 l 256,-256 q 10,-9 23,-9 13,0 23,9 9,10 9,23 0,13 -9,23 z' } ], [ 'unlock-alt', { viewBox: '0 0 1152 1536', path: 'm 1056,768 q 40,0 68,28 28,28 28,68 v 576 q 0,40 -28,68 -28,28 -68,28 H 96 Q 56,1536 28,1508 0,1480 0,1440 V 864 q 0,-40 28,-68 28,-28 68,-28 h 32 V 448 Q 128,263 259.5,131.5 391,0 576,0 761,0 892.5,131.5 1024,263 1024,448 q 0,26 -19,45 -19,19 -45,19 h -64 q -26,0 -45,-19 -19,-19 -19,-45 0,-106 -75,-181 -75,-75 -181,-75 -106,0 -181,75 -75,75 -75,181 v 320 z' } ], [ 'upload-alt', { viewBox: '0 0 1664 1600', path: 'm 1280,1408 q 0,-26 -19,-45 -19,-19 -45,-19 -26,0 -45,19 -19,19 -19,45 0,26 19,45 19,19 45,19 26,0 45,-19 19,-19 19,-45 z m 256,0 q 0,-26 -19,-45 -19,-19 -45,-19 -26,0 -45,19 -19,19 -19,45 0,26 19,45 19,19 45,19 26,0 45,-19 19,-19 19,-45 z m 128,-224 v 320 q 0,40 -28,68 -28,28 -68,28 H 96 q -40,0 -68,-28 -28,-28 -28,-68 v -320 q 0,-40 28,-68 28,-28 68,-28 h 427 q 21,56 70.5,92 49.5,36 110.5,36 h 256 q 61,0 110.5,-36 49.5,-36 70.5,-92 h 427 q 40,0 68,28 28,28 28,68 z M 1339,536 q -17,40 -59,40 h -256 v 448 q 0,26 -19,45 -19,19 -45,19 H 704 q -26,0 -45,-19 -19,-19 -19,-45 V 576 H 384 q -42,0 -59,-40 -17,-39 14,-69 L 787,19 q 18,-19 45,-19 27,0 45,19 l 448,448 q 31,30 14,69 z' } ], + [ 'volume-up', { viewBox: '0 0 1664 1422', path: 'm 768,167 v 1088 c 0,35 -29,64 -64,64 -17,0 -33,-7 -45,-19 L 326,967 H 64 C 29,967 0,938 0,903 V 519 C 0,484 29,455 64,455 H 326 L 659,122 c 12,-12 28,-19 45,-19 35,0 64,29 64,64 z m 384,544 c 0,100 -61,197 -155,235 -8,4 -17,5 -25,5 -35,0 -64,-28 -64,-64 0,-76 116,-55 116,-176 0,-121 -116,-100 -116,-176 0,-36 29,-64 64,-64 8,0 17,1 25,5 94,37 155,135 155,235 z m 256,0 c 0,203 -122,392 -310,471 -8,3 -17,5 -25,5 -36,0 -65,-29 -65,-64 0,-28 16,-47 39,-59 27,-14 52,-26 76,-44 99,-72 157,-187 157,-309 0,-122 -58,-237 -157,-309 -24,-18 -49,-30 -76,-44 -23,-12 -39,-31 -39,-59 0,-35 29,-64 64,-64 9,0 18,2 26,5 188,79 310,268 310,471 z m 256,0 c 0,307 -183,585 -465,706 -8,3 -17,5 -26,5 -35,0 -64,-29 -64,-64 0,-29 15,-45 39,-59 14,-8 30,-13 45,-21 28,-15 56,-32 82,-51 164,-121 261,-312 261,-516 0,-204 -97,-395 -261,-516 -26,-19 -54,-36 -82,-51 -15,-8 -31,-13 -45,-21 -24,-14 -39,-30 -39,-59 0,-35 29,-64 64,-64 9,0 18,2 26,5 282,121 465,399 465,706 z' } ], [ 'zoom-in', { viewBox: '0 0 1664 1664', path: 'm 1024,672 v 64 q 0,13 -9.5,22.5 Q 1005,768 992,768 H 768 v 224 q 0,13 -9.5,22.5 -9.5,9.5 -22.5,9.5 h -64 q -13,0 -22.5,-9.5 Q 640,1005 640,992 V 768 H 416 q -13,0 -22.5,-9.5 Q 384,749 384,736 v -64 q 0,-13 9.5,-22.5 Q 403,640 416,640 H 640 V 416 q 0,-13 9.5,-22.5 Q 659,384 672,384 h 64 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 v 224 h 224 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 128,32 Q 1152,519 1020.5,387.5 889,256 704,256 519,256 387.5,387.5 256,519 256,704 256,889 387.5,1020.5 519,1152 704,1152 889,1152 1020.5,1020.5 1152,889 1152,704 Z m 512,832 q 0,53 -37.5,90.5 -37.5,37.5 -90.5,37.5 -54,0 -90,-38 L 1103,1284 Q 924,1408 704,1408 561,1408 430.5,1352.5 300,1297 205.5,1202.5 111,1108 55.5,977.5 0,847 0,704 0,561 55.5,430.5 111,300 205.5,205.5 300,111 430.5,55.5 561,0 704,0 q 143,0 273.5,55.5 130.5,55.5 225,150 94.5,94.5 150,225 55.5,130.5 55.5,273.5 0,220 -124,399 l 343,343 q 37,37 37,90 z' } ], [ 'zoom-out', { viewBox: '0 0 1664 1664', path: 'm 1024,672 v 64 q 0,13 -9.5,22.5 Q 1005,768 992,768 H 416 q -13,0 -22.5,-9.5 Q 384,749 384,736 v -64 q 0,-13 9.5,-22.5 Q 403,640 416,640 h 576 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 128,32 Q 1152,519 1020.5,387.5 889,256 704,256 519,256 387.5,387.5 256,519 256,704 256,889 387.5,1020.5 519,1152 704,1152 889,1152 1020.5,1020.5 1152,889 1152,704 Z m 512,832 q 0,53 -37.5,90.5 -37.5,37.5 -90.5,37.5 -54,0 -90,-38 L 1103,1284 Q 924,1408 704,1408 561,1408 430.5,1352.5 300,1297 205.5,1202.5 111,1108 55.5,977.5 0,847 0,704 0,561 55.5,430.5 111,300 205.5,205.5 300,111 430.5,55.5 561,0 704,0 q 143,0 273.5,55.5 130.5,55.5 225,150 94.5,94.5 150,225 55.5,130.5 55.5,273.5 0,220 -124,399 l 343,343 q 37,37 37,90 z' } ], // See /img/photon.svg diff --git a/src/js/filtering-context.js b/src/js/filtering-context.js index 5bc9aa1..6642050 100644 --- a/src/js/filtering-context.js +++ b/src/js/filtering-context.js @@ -135,7 +135,6 @@ export const FilteringContext = class { } this.tstamp = 0; this.realm = ''; - this.id = undefined; this.method = 0; this.itype = NO_TYPE; this.stype = undefined; @@ -175,7 +174,6 @@ export const FilteringContext = class { fromFilteringContext(other) { this.realm = other.realm; - this.id = other.id; this.type = other.type; this.method = other.method; this.url = other.url; diff --git a/src/js/hntrie.js b/src/js/hntrie.js index e8031a6..cc726db 100644 --- a/src/js/hntrie.js +++ b/src/js/hntrie.js @@ -445,28 +445,17 @@ class HNTrieContainer { }; } - serialize(encoder) { - if ( encoder instanceof Object ) { - return encoder.encode( - this.buf32.buffer, - this.buf32[CHAR1_SLOT] - ); - } - return Array.from( - new Uint32Array( - this.buf32.buffer, - 0, - this.buf32[CHAR1_SLOT] + 3 >>> 2 - ) + toSelfie() { + return this.buf32.subarray( + 0, + this.buf32[CHAR1_SLOT] + 3 >>> 2 ); } - unserialize(selfie, decoder) { + fromSelfie(selfie) { + if ( selfie instanceof Uint32Array === false ) { return false; } this.needle = ''; - const shouldDecode = typeof selfie === 'string'; - let byteLength = shouldDecode - ? decoder.decodeSize(selfie) - : selfie.length << 2; + let byteLength = selfie.length << 2; if ( byteLength === 0 ) { return false; } byteLength = roundToPageSize(byteLength); if ( this.wasmMemory !== null ) { @@ -477,14 +466,10 @@ class HNTrieContainer { this.buf = new Uint8Array(this.wasmMemory.buffer); this.buf32 = new Uint32Array(this.buf.buffer); } - } else if ( byteLength > this.buf.length ) { - this.buf = new Uint8Array(byteLength); - this.buf32 = new Uint32Array(this.buf.buffer); - } - if ( shouldDecode ) { - decoder.decode(selfie, this.buf.buffer); - } else { this.buf32.set(selfie); + } else { + this.buf32 = selfie; + this.buf = new Uint8Array(this.buf32.buffer); } // https://github.com/uBlockOrigin/uBlock-issues/issues/2925 this.buf[255] = 0; diff --git a/src/js/i18n.js b/src/js/i18n.js index 6302b35..18c7e14 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -29,11 +29,7 @@ const i18n = ? self.browser.i18n : self.chrome.i18n; -/******************************************************************************/ - -function i18n$(...args) { - return i18n.getMessage(...args); -} +const i18n$ = (...args) => i18n.getMessage(...args); /******************************************************************************/ @@ -295,21 +291,21 @@ if ( isBackgroundProcess !== true ) { const unicodeFlagToImageSrc = new Map([ [ '🇦🇱', 'al' ], [ '🇦🇷', 'ar' ], [ '🇦🇹', 'at' ], [ '🇧🇦', 'ba' ], - [ '🇧🇬', 'bg' ], [ '🇧🇷', 'br' ], [ '🇨🇦', 'ca' ], [ '🇨🇭', 'ch' ], - [ '🇨🇳', 'cn' ], [ '🇨🇴', 'co' ], [ '🇨🇾', 'cy' ], [ '🇨🇿', 'cz' ], - [ '🇩🇪', 'de' ], [ '🇩🇰', 'dk' ], [ '🇩🇿', 'dz' ], [ '🇪🇪', 'ee' ], - [ '🇪🇬', 'eg' ], [ '🇪🇸', 'es' ], [ '🇫🇮', 'fi' ], [ '🇫🇴', 'fo' ], - [ '🇫🇷', 'fr' ], [ '🇬🇷', 'gr' ], [ '🇭🇷', 'hr' ], [ '🇭🇺', 'hu' ], - [ '🇮🇩', 'id' ], [ '🇮🇱', 'il' ], [ '🇮🇳', 'in' ], [ '🇮🇷', 'ir' ], - [ '🇮🇸', 'is' ], [ '🇮🇹', 'it' ], [ '🇯🇵', 'jp' ], [ '🇰🇷', 'kr' ], - [ '🇰🇿', 'kz' ], [ '🇱🇰', 'lk' ], [ '🇱🇹', 'lt' ], [ '🇱🇻', 'lv' ], - [ '🇲🇦', 'ma' ], [ '🇲🇩', 'md' ], [ '🇲🇰', 'mk' ], [ '🇲🇽', 'mx' ], - [ '🇲🇾', 'my' ], [ '🇳🇱', 'nl' ], [ '🇳🇴', 'no' ], [ '🇳🇵', 'np' ], - [ '🇵🇱', 'pl' ], [ '🇵🇹', 'pt' ], [ '🇷🇴', 'ro' ], [ '🇷🇸', 'rs' ], - [ '🇷🇺', 'ru' ], [ '🇸🇦', 'sa' ], [ '🇸🇮', 'si' ], [ '🇸🇰', 'sk' ], - [ '🇸🇪', 'se' ], [ '🇸🇷', 'sr' ], [ '🇹🇭', 'th' ], [ '🇹🇯', 'tj' ], - [ '🇹🇼', 'tw' ], [ '🇹🇷', 'tr' ], [ '🇺🇦', 'ua' ], [ '🇺🇿', 'uz' ], - [ '🇻🇳', 'vn' ], [ '🇽🇰', 'xk' ], + [ '🇧🇪', 'be' ], [ '🇧🇬', 'bg' ], [ '🇧🇷', 'br' ], [ '🇨🇦', 'ca' ], + [ '🇨🇭', 'ch' ], [ '🇨🇳', 'cn' ], [ '🇨🇴', 'co' ], [ '🇨🇾', 'cy' ], + [ '🇨🇿', 'cz' ], [ '🇩🇪', 'de' ], [ '🇩🇰', 'dk' ], [ '🇩🇿', 'dz' ], + [ '🇪🇪', 'ee' ], [ '🇪🇬', 'eg' ], [ '🇪🇸', 'es' ], [ '🇫🇮', 'fi' ], + [ '🇫🇴', 'fo' ], [ '🇫🇷', 'fr' ], [ '🇬🇷', 'gr' ], [ '🇭🇷', 'hr' ], + [ '🇭🇺', 'hu' ], [ '🇮🇩', 'id' ], [ '🇮🇱', 'il' ], [ '🇮🇳', 'in' ], + [ '🇮🇷', 'ir' ], [ '🇮🇸', 'is' ], [ '🇮🇹', 'it' ], [ '🇯🇵', 'jp' ], + [ '🇰🇷', 'kr' ], [ '🇰🇿', 'kz' ], [ '🇱🇰', 'lk' ], [ '🇱🇹', 'lt' ], + [ '🇱🇻', 'lv' ], [ '🇲🇦', 'ma' ], [ '🇲🇩', 'md' ], [ '🇲🇰', 'mk' ], + [ '🇲🇽', 'mx' ], [ '🇲🇾', 'my' ], [ '🇳🇱', 'nl' ], [ '🇳🇴', 'no' ], + [ '🇳🇵', 'np' ], [ '🇵🇱', 'pl' ], [ '🇵🇹', 'pt' ], [ '🇷🇴', 'ro' ], + [ '🇷🇸', 'rs' ], [ '🇷🇺', 'ru' ], [ '🇸🇦', 'sa' ], [ '🇸🇮', 'si' ], + [ '🇸🇰', 'sk' ], [ '🇸🇪', 'se' ], [ '🇸🇷', 'sr' ], [ '🇹🇭', 'th' ], + [ '🇹🇯', 'tj' ], [ '🇹🇼', 'tw' ], [ '🇹🇷', 'tr' ], [ '🇺🇦', 'ua' ], + [ '🇺🇿', 'uz' ], [ '🇻🇳', 'vn' ], [ '🇽🇰', 'xk' ], ]); const reUnicodeFlags = new RegExp( Array.from(unicodeFlagToImageSrc).map(a => a[0]).join('|'), diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index 177632e..b7aeb8e 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -21,6 +21,7 @@ 'use strict'; +import { broadcast } from './broadcast.js'; import { hostnameFromURI } from './uri-utils.js'; import { i18n, i18n$ } from './i18n.js'; import { dom, qs$, qsa$ } from './dom.js'; @@ -33,8 +34,9 @@ import { dom, qs$, qsa$ } from './dom.js'; const messaging = vAPI.messaging; const logger = self.logger = { ownerId: Date.now() }; const logDate = new Date(); -const logDateTimezoneOffset = logDate.getTimezoneOffset() * 60000; +const logDateTimezoneOffset = logDate.getTimezoneOffset() * 60; const loggerEntries = []; +let loggerEntryIdGenerator = 1; const COLUMN_TIMESTAMP = 0; const COLUMN_FILTER = 1; @@ -318,13 +320,11 @@ const LogEntry = function(details) { if ( details instanceof Object === false ) { return; } const receiver = LogEntry.prototype; for ( const prop in receiver ) { - if ( - details.hasOwnProperty(prop) && - details[prop] !== receiver[prop] - ) { - this[prop] = details[prop]; - } + if ( details.hasOwnProperty(prop) === false ) { continue; } + if ( details[prop] === receiver[prop] ) { continue; } + this[prop] = details[prop]; } + this.id = `${loggerEntryIdGenerator++}`; if ( details.aliasURL !== undefined ) { this.aliased = true; } @@ -345,7 +345,6 @@ LogEntry.prototype = { docHostname: '', domain: '', filter: undefined, - id: '', method: '', realm: '', tabDomain: '', @@ -368,7 +367,7 @@ const createLogSeparator = function(details, text) { separator.textContent = ''; const textContent = []; - logDate.setTime(separator.tstamp - logDateTimezoneOffset); + logDate.setTime((separator.tstamp - logDateTimezoneOffset) * 1000); textContent.push( // cell 0 padTo2(logDate.getUTCHours()) + ':' + @@ -377,7 +376,7 @@ const createLogSeparator = function(details, text) { // cell 1 text ); - separator.textContent = textContent.join('\t'); + separator.textContent = textContent.join('\x1F'); if ( details.voided ) { separator.voided = true; @@ -464,7 +463,7 @@ const parseLogEntry = function(details) { const textContent = []; // Cell 0 - logDate.setTime(details.tstamp - logDateTimezoneOffset); + logDate.setTime((details.tstamp - logDateTimezoneOffset) * 1000); textContent.push( padTo2(logDate.getUTCHours()) + ':' + padTo2(logDate.getUTCMinutes()) + ':' + @@ -474,7 +473,13 @@ const parseLogEntry = function(details) { // Cell 1 if ( details.realm === 'message' ) { textContent.push(details.text); - entry.textContent = textContent.join('\t'); + if ( details.type ) { + textContent.push(details.type); + } + if ( details.keywords ) { + textContent.push(...details.keywords); + } + entry.textContent = textContent.join('\x1F') + '\x1F'; return entry; } @@ -545,7 +550,7 @@ const parseLogEntry = function(details) { textContent.push(`aliasURL=${details.aliasURL}`); } - entry.textContent = textContent.join('\t'); + entry.textContent = textContent.join('\x1F'); return entry; }; @@ -744,7 +749,7 @@ const viewPort = (( ) => { vwEntry.logEntry = details; - const cells = details.textContent.split('\t'); + const cells = details.textContent.split('\x1F'); const div = dom.clone(vwLogEntryTemplate); const divcl = div.classList; let span; @@ -863,7 +868,7 @@ const viewPort = (( ) => { // Alias URL (CNAME, etc.) if ( cells.length > 8 ) { - const pos = details.textContent.lastIndexOf('\taliasURL='); + const pos = details.textContent.lastIndexOf('\x1FaliasURL='); if ( pos !== -1 ) { dom.attr(div, 'data-aliasid', details.id); } @@ -1336,9 +1341,7 @@ dom.on(document, 'keydown', ev => { if ( reSchemeOnly.test(value) ) { value = `|${value}`; } else { - if ( value.endsWith('/') ) { - value += '*'; - } else if ( /[/?]/.test(value) === false ) { + if ( /[/?]/.test(value) === false ) { value += '^'; } value = `||${value}`; @@ -1410,7 +1413,8 @@ dom.on(document, 'keydown', ev => { // Create static filter if ( target.id === 'createStaticFilter' ) { ev.stopPropagation(); - const value = staticFilterNode().value; + const value = staticFilterNode().value + .replace(/^((?:@@)?\/.+\/)(\$|$)/, '$1*$2'); // Avoid duplicates if ( createdStaticFilters.hasOwnProperty(value) ) { return; } createdStaticFilters[value] = true; @@ -1620,9 +1624,10 @@ dom.on(document, 'keydown', ev => { const aliasURLFromID = function(id) { if ( id === '' ) { return ''; } for ( const entry of loggerEntries ) { - if ( entry.id !== id || entry.aliased ) { continue; } - const fields = entry.textContent.split('\t'); - return fields[COLUMN_URL] || ''; + if ( entry.id !== id ) { continue; } + const match = /\baliasURL=([^\x1F]+)/.exec(entry.textContent); + if ( match === null ) { return ''; } + return match[1]; } return ''; }; @@ -2005,8 +2010,12 @@ dom.on(document, 'keydown', ev => { }; const toggleOn = async function(ev) { - targetRow = ev.target.closest('.canDetails'); - if ( targetRow === null ) { return; } + const clickedRow = ev.target.closest('.canDetails'); + if ( clickedRow === null ) { return; } + if ( clickedRow === targetRow ) { + return toggleOff(); + } + targetRow = clickedRow; ev.stopPropagation(); targetTabId = tabIdFromAttribute(targetRow); targetType = targetRow.children[COLUMN_TYPE].textContent.trim() || ''; @@ -2052,12 +2061,30 @@ dom.on(document, 'keydown', ev => { } }); - dom.on( - '#netInspector', - 'click', - '.canDetails > span:not(:nth-of-type(4)):not(:nth-of-type(8))', - ev => { toggleOn(ev); } - ); + // This is to detect text selection, in which case the click won't be + // interpreted as a request to open the details of the entry. + let selectionAtMouseDown; + let selectionAtTimer; + dom.on('#netInspector', 'mousedown', '.canDetails *', ev => { + if ( ev.button !== 0 ) { return; } + if ( selectionAtMouseDown !== undefined ) { return; } + selectionAtMouseDown = document.getSelection().toString(); + }); + + dom.on('#netInspector', 'click', '.canDetails *', ev => { + if ( ev.button !== 0 ) { return; } + if ( selectionAtTimer !== undefined ) { + clearTimeout(selectionAtTimer); + } + selectionAtTimer = setTimeout(( ) => { + selectionAtTimer = undefined; + const selectionAsOfNow = document.getSelection().toString(); + const selectionHasChanged = selectionAsOfNow !== selectionAtMouseDown; + selectionAtMouseDown = undefined; + if ( selectionHasChanged && selectionAsOfNow !== '' ) { return; } + toggleOn(ev); + }, 333); + }); dom.on( '#netInspector', @@ -2149,16 +2176,12 @@ const rowFilterer = (( ) => { filters = builtinFilters.concat(userFilters); }; - const filterOne = function(logEntry) { - if ( - logEntry.dead || - selectedTabId !== 0 && - ( - logEntry.tabId === undefined || - logEntry.tabId > 0 && logEntry.tabId !== selectedTabId - ) - ) { - return false; + const filterOne = logEntry => { + if ( logEntry.dead ) { return false; } + if ( selectedTabId !== 0 ) { + if ( logEntry.tabId !== undefined && logEntry.tabId > 0 ) { + if (logEntry.tabId !== selectedTabId ) { return false; } + } } if ( masterFilterSwitch === false || filters.length === 0 ) { @@ -2303,7 +2326,7 @@ const rowJanitor = (( ) => { ? opts.maxEntryCount : 0; const obsolete = typeof opts.maxAge === 'number' - ? Date.now() - opts.maxAge * 60000 + ? Date.now() / 1000 - opts.maxAge * 60 : 0; let i = rowIndex; @@ -2682,16 +2705,16 @@ const loggerStats = (( ) => { const text = entry.textContent; const fields = []; let i = 0; - let beg = text.indexOf('\t'); + let beg = text.indexOf('\x1F'); if ( beg === 0 ) { continue; } let timeField = text.slice(0, beg); if ( options.time === 'anonymous' ) { - timeField = '+' + Math.round((entry.tstamp - t0) / 1000).toString(); + timeField = '+' + Math.round(entry.tstamp - t0).toString(); } fields.push(timeField); beg += 1; while ( beg < text.length ) { - let end = text.indexOf('\t', beg); + let end = text.indexOf('\x1F', beg); if ( end === -1 ) { end = text.length; } fields.push(text.slice(beg, end)); beg = end + 1; @@ -3020,6 +3043,19 @@ dom.on('#pageSelector', 'change', pageSelectorChanged); dom.on('#netInspector .vCompactToggler', 'click', toggleVCompactView); dom.on('#pause', 'click', pauseNetInspector); +dom.on('#logLevel', 'click', ev => { + const level = dom.cl.toggle(ev.currentTarget, 'active') ? 2 : 1; + broadcast({ what: 'loggerLevelChanged', level }); +}); + +dom.on('#netInspector #vwContent', 'copy', ev => { + const selection = document.getSelection(); + const text = selection.toString(); + if ( /\x1F|\u200B/.test(text) === false ) { return; } + ev.clipboardData.setData('text/plain', text.replace(/\x1F|\u200B/g, '\t')); + ev.preventDefault(); +}); + // https://github.com/gorhill/uBlock/issues/507 // Ensure tab selector is in sync with URL hash pageSelectorFromURLHash(); diff --git a/src/js/logger.js b/src/js/logger.js index 5d1114f..766188e 100644 --- a/src/js/logger.js +++ b/src/js/logger.js @@ -23,7 +23,7 @@ /******************************************************************************/ -import { broadcastToAll } from './broadcast.js'; +import { broadcast, broadcastToAll } from './broadcast.js'; /******************************************************************************/ @@ -47,34 +47,38 @@ const janitorTimer = vAPI.defer.create(( ) => { broadcastToAll({ what: 'loggerDisabled' }); }); -const boxEntry = function(details) { - if ( details.tstamp === undefined ) { - details.tstamp = Date.now(); - } +const boxEntry = details => { + details.tstamp = Date.now() / 1000 | 0; return JSON.stringify(details); }; +const pushOne = box => { + if ( writePtr !== 0 && box === buffer[writePtr-1] ) { return; } + if ( writePtr === buffer.length ) { + buffer.push(box); + } else { + buffer[writePtr] = box; + } + writePtr += 1; +}; + const logger = { enabled: false, ownerId: undefined, - writeOne: function(details) { + writeOne(details) { if ( buffer === null ) { return; } - const box = boxEntry(details); - if ( writePtr === buffer.length ) { - buffer.push(box); - } else { - buffer[writePtr] = box; - } - writePtr += 1; + pushOne(boxEntry(details)); }, - readAll: function(ownerId) { + readAll(ownerId) { this.ownerId = ownerId; if ( buffer === null ) { this.enabled = true; buffer = []; janitorTimer.on(logBufferObsoleteAfter); + broadcast({ what: 'loggerEnabled' }); } const out = buffer.slice(0, writePtr); + buffer.fill('', 0, writePtr); writePtr = 0; lastReadTime = Date.now(); return out; diff --git a/src/js/messaging.js b/src/js/messaging.js index 52242b3..5f39af4 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -45,6 +45,7 @@ import { dnrRulesetFromRawLists } from './static-dnr-filtering.js'; import { i18n$ } from './i18n.js'; import { redirectEngine } from './redirect-engine.js'; import * as sfp from './static-filtering-parser.js'; +import * as s14e from './s14e-serializer.js'; import { permanentFirewall, @@ -63,8 +64,6 @@ import { isNetworkURI, } from './uri-utils.js'; -import './benchmarks.js'; - /******************************************************************************/ // https://github.com/uBlockOrigin/uBlock-issues/issues/710 @@ -364,11 +363,12 @@ const popupDataFromTabId = function(tabId, tabTitle) { colorBlindFriendly: µbus.colorBlindFriendly, cosmeticFilteringSwitch: false, firewallPaneMinimized: µbus.firewallPaneMinimized, - globalAllowedRequestCount: µb.localSettings.allowedRequestCount, - globalBlockedRequestCount: µb.localSettings.blockedRequestCount, + globalAllowedRequestCount: µb.requestStats.allowedCount, + globalBlockedRequestCount: µb.requestStats.blockedCount, fontSize: µbhs.popupFontSize, godMode: µbhs.filterAuthorMode, netFilteringSwitch: false, + userFiltersAreEnabled: µb.userFiltersAreEnabled(), rawURL: tabContext.rawURL, pageURL: tabContext.normalURL, pageHostname: rootHostname, @@ -378,6 +378,7 @@ const popupDataFromTabId = function(tabId, tabTitle) { popupPanelDisabledSections: µbhs.popupPanelDisabledSections, popupPanelLockedSections: µbhs.popupPanelLockedSections, popupPanelHeightMode: µbhs.popupPanelHeightMode, + popupPanelOrientation: µbhs.popupPanelOrientation, tabId, tabTitle, tooltipsDisabled: µbus.tooltipsDisabled, @@ -715,15 +716,15 @@ const retrieveContentScriptParameters = async function(sender, request) { // https://github.com/uBlockOrigin/uBlock-issues/issues/688#issuecomment-748179731 // For non-network URIs, scriptlet injection is deferred to here. The // effective URL is available here in `request.url`. - if ( logger.enabled || request.needScriptlets ) { - const scriptletDetails = scriptletFilteringEngine.injectNow(request); + if ( logger.enabled ) { + const scriptletDetails = scriptletFilteringEngine.retrieve(request); if ( scriptletDetails !== undefined ) { scriptletFilteringEngine.toLogger(request, scriptletDetails); - if ( request.needScriptlets ) { - response.scriptletDetails = scriptletDetails; - } } } + if ( request.needScriptlets ) { + scriptletFilteringEngine.injectNow(request); + } // https://github.com/NanoMeow/QuickReports/issues/6#issuecomment-414516623 // Inject as early as possible to make the cosmetic logger code less @@ -795,6 +796,17 @@ const onMessage = function(request, sender, callback) { µb.maybeGoodPopup.url = request.url; break; + case 'messageToLogger': + if ( logger.enabled !== true ) { break; } + logger.writeOne({ + tabId: sender.tabId, + realm: 'message', + type: request.type || 'info', + keywords: [ 'scriptlet' ], + text: request.text, + }); + break; + case 'shouldRenderNoscriptTags': if ( pageStore === null ) { break; } const fctxt = µb.filteringContext.fromTabId(sender.tabId); @@ -913,21 +925,6 @@ const fromBase64 = function(encoded) { return Promise.resolve(u8array !== undefined ? u8array : encoded); }; -const toBase64 = function(data) { - const value = data instanceof Uint8Array - ? denseBase64.encode(data) - : data; - return Promise.resolve(value); -}; - -const compress = function(json) { - return lz4Codec.encode(json, toBase64); -}; - -const decompress = function(encoded) { - return lz4Codec.decode(encoded, fromBase64); -}; - const onMessage = function(request, sender, callback) { // Cloud storage support is optional. if ( µb.cloudStorageSupported !== true ) { @@ -949,15 +946,25 @@ const onMessage = function(request, sender, callback) { return; case 'cloudPull': - request.decode = decompress; + request.decode = encoded => { + if ( s14e.isSerialized(encoded) ) { + return s14e.deserializeAsync(encoded, { thread: true }); + } + // Legacy decoding: needs to be kept around for the foreseeable future. + return lz4Codec.decode(encoded, fromBase64); + }; return vAPI.cloud.pull(request).then(result => { callback(result); }); case 'cloudPush': - if ( µb.hiddenSettings.cloudStorageCompression ) { - request.encode = compress; - } + request.encode = data => { + const options = { + compress: µb.hiddenSettings.cloudStorageCompression, + thread: true, + }; + return s14e.serializeAsync(data, options); + }; return vAPI.cloud.push(request).then(result => { callback(result); }); @@ -1444,11 +1451,23 @@ const onMessage = function(request, sender, callback) { case 'readUserFilters': return µb.loadUserFilters().then(result => { - result.trustedSource = µb.isTrustedList(µb.userFiltersPath); + result.enabled = µb.selectedFilterLists.includes(µb.userFiltersPath); + result.trusted = µb.isTrustedList(µb.userFiltersPath); callback(result); }); case 'writeUserFilters': + if ( request.enabled ) { + µb.applyFilterListSelection({ + toSelect: [ µb.userFiltersPath ], + merge: true, + }); + } else { + µb.applyFilterListSelection({ + toRemove: [ µb.userFiltersPath ], + }); + } + µb.changeUserSettings('userFiltersTrusted', request.trusted || false); return µb.saveUserFilters(request.content).then(result => { callback(result); }); @@ -1839,8 +1858,26 @@ const onMessage = function(request, sender, callback) { return; case 'snfeBenchmark': - µb.benchmarkStaticNetFiltering({ redirectEngine }).then(result => { - callback(result); + import('/js/benchmarks.js').then(module => { + module.benchmarkStaticNetFiltering({ redirectEngine }).then(result => { + callback(result); + }); + }); + return; + + case 'cfeBenchmark': + import('/js/benchmarks.js').then(module => { + module.benchmarkCosmeticFiltering().then(result => { + callback(result); + }); + }); + return; + + case 'sfeBenchmark': + import('/js/benchmarks.js').then(module => { + module.benchmarkScriptletFiltering().then(result => { + callback(result); + }); }); return; diff --git a/src/js/pagestore.js b/src/js/pagestore.js index 907e747..227352d 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -19,17 +19,13 @@ Home: https://github.com/gorhill/uBlock */ -'use strict'; - /******************************************************************************/ -import contextMenu from './contextmenu.js'; -import logger from './logger.js'; -import staticNetFilteringEngine from './static-net-filtering.js'; -import µb from './background.js'; -import webext from './webext.js'; -import { orphanizeString } from './text-utils.js'; -import { redirectEngine } from './redirect-engine.js'; +import { + domainFromHostname, + hostnameFromURI, + isNetworkURI, +} from './uri-utils.js'; import { sessionFirewall, @@ -37,11 +33,13 @@ import { sessionURLFiltering, } from './filtering-engines.js'; -import { - domainFromHostname, - hostnameFromURI, - isNetworkURI, -} from './uri-utils.js'; +import contextMenu from './contextmenu.js'; +import logger from './logger.js'; +import { orphanizeString } from './text-utils.js'; +import { redirectEngine } from './redirect-engine.js'; +import staticNetFilteringEngine from './static-net-filtering.js'; +import webext from './webext.js'; +import µb from './background.js'; /******************************************************************************* @@ -379,11 +377,13 @@ const PageStore = class { // If we are navigating from-to same site, remember whether large // media elements were temporarily allowed. - if ( - typeof this.allowLargeMediaElementsUntil !== 'number' || - tabContext.rootHostname !== this.tabHostname - ) { - this.allowLargeMediaElementsUntil = Date.now(); + const now = Date.now(); + if ( typeof this.allowLargeMediaElementsUntil !== 'number' ) { + this.allowLargeMediaElementsUntil = now; + } else if ( tabContext.rootHostname !== this.tabHostname ) { + if ( this.tabHostname.endsWith('about-scheme') === false ) { + this.allowLargeMediaElementsUntil = now; + } } this.tabHostname = tabContext.rootHostname; @@ -739,10 +739,8 @@ const PageStore = class { aggregateAllowed += 1; } } - if ( aggregateAllowed !== 0 || aggregateBlocked !== 0 ) { - µb.localSettings.blockedRequestCount += aggregateBlocked; - µb.localSettings.allowedRequestCount += aggregateAllowed; - µb.localSettingsLastModified = now; + if ( aggregateAllowed || aggregateBlocked ) { + µb.incrementRequestStats(aggregateBlocked, aggregateAllowed); } journal.length = 0; } diff --git a/src/js/popup-fenix.js b/src/js/popup-fenix.js index b44b923..9f2af08 100644 --- a/src/js/popup-fenix.js +++ b/src/js/popup-fenix.js @@ -70,6 +70,9 @@ let cachedPopupHash = ''; const reCyrillicNonAmbiguous = /[\u0400-\u042b\u042d-\u042f\u0431\u0432\u0434\u0436-\u043d\u0442\u0444\u0446-\u0449\u044b-\u0454\u0457\u0459-\u0460\u0462-\u0474\u0476-\u04ba\u04bc\u04be-\u04ce\u04d0-\u0500\u0502-\u051a\u051c\u051e-\u052f]/; const reCyrillicAmbiguous = /[\u042c\u0430\u0433\u0435\u043e\u043f\u0440\u0441\u0443\u0445\u044a\u0455\u0456\u0458\u0461\u0475\u04bb\u04bd\u04cf\u0501\u051b\u051d]/; +const hasOwnProperty = (o, p) => + Object.prototype.hasOwnProperty.call(o, p); + /******************************************************************************/ const cachePopupData = function(data) { @@ -88,7 +91,7 @@ const cachePopupData = function(data) { return popupData; } for ( const hostname in hostnameDict ) { - if ( hostnameDict.hasOwnProperty(hostname) === false ) { continue; } + if ( hasOwnProperty(hostnameDict, hostname) === false ) { continue; } let domain = hostnameDict[hostname].domain; let prefix = hostname.slice(0, 0 - domain.length - 1); // Prefix with space char for 1st-party hostnames: this ensure these @@ -160,7 +163,7 @@ const formatNumber = function(count) { }); if ( intl.resolvedOptions instanceof Function && - intl.resolvedOptions().hasOwnProperty('notation') + hasOwnProperty(intl.resolvedOptions(), 'notation') ) { intlNumberFormat = intl; } @@ -545,7 +548,7 @@ const renderPrivacyExposure = function() { if ( des === '*' || desHostnameDone.has(des) ) { continue; } const hnDetails = hostnameDict[des]; const { domain, counts } = hnDetails; - if ( allDomains.hasOwnProperty(domain) === false ) { + if ( hasOwnProperty(allDomains, domain) === false ) { allDomains[domain] = false; allDomainCount += 1; } @@ -614,11 +617,11 @@ const renderPopup = function() { } } - dom.cl.toggle( - '#basicTools', - 'canPick', - popupData.canElementPicker === true && isFiltering - ); + const canPick = popupData.canElementPicker && isFiltering; + + dom.cl.toggle('#gotoZap', 'canPick', canPick); + dom.cl.toggle('#gotoPick', 'canPick', canPick && popupData.userFiltersAreEnabled); + dom.cl.toggle('#gotoReport', 'canPick', canPick); let blocked, total; if ( popupData.pageCounts !== undefined ) { @@ -675,7 +678,7 @@ const renderPopup = function() { total ? Math.min(total, 99).toLocaleString() : '' ); - // Unprocesseed request(s) warning + // Unprocessed request(s) warning dom.cl.toggle(dom.root, 'warn', popupData.hasUnprocessedRequest === true); dom.cl.toggle(dom.html, 'colorBlind', popupData.colorBlindFriendly === true); @@ -802,7 +805,7 @@ let renderOnce = function() { dom.attr('#firewall [title][data-src]', 'title', null); } - // This must be done the firewall is populated + // This must be done when the firewall is populated if ( popupData.popupPanelHeightMode === 1 ) { dom.cl.add(dom.body, 'vMin'); } @@ -1462,6 +1465,33 @@ const getPopupData = async function(tabId, first = false) { } }; + const setOrientation = async ( ) => { + if ( dom.cl.has(dom.root, 'mobile') ) { + dom.cl.remove(dom.root, 'desktop'); + dom.cl.add(dom.root, 'portrait'); + return; + } + if ( selfURL.searchParams.get('portrait') !== null ) { + dom.cl.remove(dom.root, 'desktop'); + dom.cl.add(dom.root, 'portrait'); + return; + } + if ( popupData.popupPanelOrientation === 'landscape' ) { return; } + if ( popupData.popupPanelOrientation === 'portrait' ) { + dom.cl.remove(dom.root, 'desktop'); + dom.cl.add(dom.root, 'portrait'); + return; + } + if ( dom.cl.has(dom.root, 'desktop') === false ) { return; } + await nextFrames(8); + const main = qs$('#main'); + const firewall = qs$('#firewall'); + const minWidth = (main.offsetWidth + firewall.offsetWidth) / 1.1; + if ( window.innerWidth < minWidth ) { + dom.cl.add(dom.root, 'portrait'); + } + }; + // The purpose of the following code is to reset to a vertical layout // should the viewport not be enough wide to accommodate the horizontal // layout. @@ -1474,24 +1504,7 @@ const getPopupData = async function(tabId, first = false) { // Use a tolerance proportional to the sum of the width of the panes // when testing against viewport width. const checkViewport = async function() { - if ( - dom.cl.has(dom.root, 'mobile') || - selfURL.searchParams.get('portrait') - ) { - dom.cl.add(dom.root, 'portrait'); - dom.cl.remove(dom.root, 'desktop'); - } else if ( dom.cl.has(dom.root, 'desktop') ) { - await nextFrames(8); - const main = qs$('#main'); - const firewall = qs$('#firewall'); - const minWidth = (main.offsetWidth + firewall.offsetWidth) / 1.1; - if ( - selfURL.searchParams.get('portrait') || - window.innerWidth < minWidth - ) { - dom.cl.add(dom.root, 'portrait'); - } - } + await setOrientation(); if ( dom.cl.has(dom.root, 'portrait') ) { const panes = qs$('#panes'); const sticky = qs$('#sticky'); diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index 2f58066..1edb376 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -24,11 +24,7 @@ /******************************************************************************/ import redirectableResources from './redirect-resources.js'; - -import { - LineIterator, - orphanizeString, -} from './text-utils.js'; +import { LineIterator, orphanizeString } from './text-utils.js'; /******************************************************************************/ @@ -76,7 +72,7 @@ const warSecret = typeof vAPI === 'object' && vAPI !== null : ( ) => ''; const RESOURCES_SELFIE_VERSION = 7; -const RESOURCES_SELFIE_NAME = 'compiled/redirectEngine/resources'; +const RESOURCES_SELFIE_NAME = 'selfie/redirectEngine/resources'; /******************************************************************************/ /******************************************************************************/ @@ -448,33 +444,22 @@ class RedirectEngine { } selfieFromResources(storage) { - storage.put( - RESOURCES_SELFIE_NAME, - JSON.stringify({ - version: RESOURCES_SELFIE_VERSION, - aliases: Array.from(this.aliases), - resources: Array.from(this.resources), - }) - ); + return storage.toCache(RESOURCES_SELFIE_NAME, { + version: RESOURCES_SELFIE_VERSION, + aliases: this.aliases, + resources: this.resources, + }); } async resourcesFromSelfie(storage) { - const result = await storage.get(RESOURCES_SELFIE_NAME); - let selfie; - try { - selfie = JSON.parse(result.content); - } catch(ex) { - } - if ( - selfie instanceof Object === false || - selfie.version !== RESOURCES_SELFIE_VERSION || - Array.isArray(selfie.resources) === false - ) { - return false; - } - this.aliases = new Map(selfie.aliases); - this.resources = new Map(); - for ( const [ token, entry ] of selfie.resources ) { + const selfie = await storage.fromCache(RESOURCES_SELFIE_NAME); + if ( selfie instanceof Object === false ) { return false; } + if ( selfie.version !== RESOURCES_SELFIE_VERSION ) { return false; } + if ( selfie.aliases instanceof Map === false ) { return false; } + if ( selfie.resources instanceof Map === false ) { return false; } + this.aliases = selfie.aliases; + this.resources = selfie.resources; + for ( const [ token, entry ] of this.resources ) { this.resources.set(token, RedirectEntry.fromDetails(entry)); } return true; diff --git a/src/js/reverselookup.js b/src/js/reverselookup.js index c21ca4b..e7bf24e 100644 --- a/src/js/reverselookup.js +++ b/src/js/reverselookup.js @@ -62,7 +62,7 @@ const stopWorker = function() { }; const workerTTLTimer = vAPI.defer.create(stopWorker); -const workerTTL = { min: 5 }; +const workerTTL = { min: 1.5 }; const initWorker = function() { if ( worker === null ) { diff --git a/src/js/s14e-serializer.js b/src/js/s14e-serializer.js new file mode 100644 index 0000000..aae0ac9 --- /dev/null +++ b/src/js/s14e-serializer.js @@ -0,0 +1,1405 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2024-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +'use strict'; + +/******************************************************************************* + * + * Structured-Cloneable to Unicode-Only SERIALIZER + * + * Purpose: + * + * Serialize/deserialize arbitrary JS data to/from well-formed Unicode strings. + * + * The browser does not expose an API to serialize structured-cloneable types + * into a single string. JSON.stringify() does not support complex JavaScript + * objects, and does not support references to composite types. Unless the + * data to serialize is only JS strings, it is difficult to easily switch + * from one type of storage to another. + * + * Serializing to a well-formed Unicode string allows to store structured- + * cloneable data to any storage. Not all storages support storing binary data, + * but all storages support storing Unicode strings. + * + * Structured-cloneable types: + * https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types + * + * ----------------+------------------+------------------+---------------------- + * Data types | String | JSONable | structured-cloneable + * ================+============================================================ + * document.cookie | Yes | No | No + * ----------------+------------------+------------------+---------------------- + * localStorage | Yes | No | No + * ----------------+------------------+------------------+---------------------- + * IndexedDB | Yes | Yes | Yes + * ----------------+------------------+------------------+---------------------- + * browser.storage | Yes | Yes | No + * ----------------+------------------+------------------+---------------------- + * Cache API | Yes | No | No + * ----------------+------------------+------------------+---------------------- + * + * The above table shows that only JS strings can be persisted natively to all + * types of storage. The purpose of this library is to convert + * structure-cloneable data (which is a superset of JSONable data) into a + * single JS string. The resulting string is meant to be as small as possible. + * As a result, it is not human-readable, though it contains only printable + * ASCII characters -- and possibly Unicode characters beyond ASCII. + * + * The resulting JS string will not contain characters which require escaping + * should it be converted to a JSON value. However it may contain characters + * which require escaping should it be converted to a URI component. + * + * Characteristics: + * + * - Serializes/deserializes data to/from a single well-formed Unicode string + * - Strings do not require escaping, i.e. they are stored as-is + * - Supports multiple references to same object + * - Supports reference cycles + * - Supports synchronous and asynchronous API + * - Supports usage of Worker + * - Optionally supports LZ4 compression + * + * TODO: + * + * - Harden against unexpected conditions, such as corrupted string during + * deserialization. + * - Evaluate supporting checksum. + * + * */ + +const VERSION = 1; +const SEPARATORCHAR = ' '; +const SEPARATORCHARCODE = SEPARATORCHAR.charCodeAt(0); +const SENTINELCHAR = '!'; +const SENTINELCHARCODE = SENTINELCHAR.charCodeAt(0); +const MAGICPREFIX = `UOSC_${VERSION}${SEPARATORCHAR}`; +const MAGICLZ4PREFIX = `UOSC/lz4_${VERSION}${SEPARATORCHAR}`; +const FAILMARK = Number.MAX_SAFE_INTEGER; +// Avoid characters which require escaping when serialized to JSON: +const SAFECHARS = "&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~"; +const NUMSAFECHARS = SAFECHARS.length; +const BITS_PER_SAFECHARS = Math.log2(NUMSAFECHARS); + +const { intToChar, intToCharCode, charCodeToInt } = (( ) => { + const intToChar = []; + const intToCharCode = []; + const charCodeToInt = []; + for ( let i = 0; i < NUMSAFECHARS; i++ ) { + intToChar[i] = SAFECHARS.charAt(i); + intToCharCode[i] = SAFECHARS.charCodeAt(i); + charCodeToInt[i] = 0; + } + for ( let i = NUMSAFECHARS; i < 128; i++ ) { + intToChar[i] = ''; + intToCharCode[i] = 0; + charCodeToInt[i] = 0; + } + for ( let i = 0; i < SAFECHARS.length; i++ ) { + charCodeToInt[SAFECHARS.charCodeAt(i)] = i; + } + return { intToChar, intToCharCode, charCodeToInt }; +})(); + +let iota = 1; +const I_STRING_SMALL = iota++; +const I_STRING_LARGE = iota++; +const I_ZERO = iota++; +const I_INTEGER_SMALL_POS = iota++; +const I_INTEGER_SMALL_NEG = iota++; +const I_INTEGER_LARGE_POS = iota++; +const I_INTEGER_LARGE_NEG = iota++; +const I_BOOL_FALSE = iota++; +const I_BOOL_TRUE = iota++; +const I_NULL = iota++; +const I_UNDEFINED = iota++; +const I_FLOAT = iota++; +const I_REGEXP = iota++; +const I_DATE = iota++; +const I_REFERENCE = iota++; +const I_OBJECT_SMALL = iota++; +const I_OBJECT_LARGE = iota++; +const I_ARRAY_SMALL = iota++; +const I_ARRAY_LARGE = iota++; +const I_SET_SMALL = iota++; +const I_SET_LARGE = iota++; +const I_MAP_SMALL = iota++; +const I_MAP_LARGE = iota++; +const I_ARRAYBUFFER = iota++; +const I_INT8ARRAY = iota++; +const I_UINT8ARRAY = iota++; +const I_UINT8CLAMPEDARRAY = iota++; +const I_INT16ARRAY = iota++; +const I_UINT16ARRAY = iota++; +const I_INT32ARRAY = iota++; +const I_UINT32ARRAY = iota++; +const I_FLOAT32ARRAY = iota++; +const I_FLOAT64ARRAY = iota++; +const I_DATAVIEW = iota++; + +const C_STRING_SMALL = intToChar[I_STRING_SMALL]; +const C_STRING_LARGE = intToChar[I_STRING_LARGE]; +const C_ZERO = intToChar[I_ZERO]; +const C_INTEGER_SMALL_POS = intToChar[I_INTEGER_SMALL_POS]; +const C_INTEGER_SMALL_NEG = intToChar[I_INTEGER_SMALL_NEG]; +const C_INTEGER_LARGE_POS = intToChar[I_INTEGER_LARGE_POS]; +const C_INTEGER_LARGE_NEG = intToChar[I_INTEGER_LARGE_NEG]; +const C_BOOL_FALSE = intToChar[I_BOOL_FALSE]; +const C_BOOL_TRUE = intToChar[I_BOOL_TRUE]; +const C_NULL = intToChar[I_NULL]; +const C_UNDEFINED = intToChar[I_UNDEFINED]; +const C_FLOAT = intToChar[I_FLOAT]; +const C_REGEXP = intToChar[I_REGEXP]; +const C_DATE = intToChar[I_DATE]; +const C_REFERENCE = intToChar[I_REFERENCE]; +const C_OBJECT_SMALL = intToChar[I_OBJECT_SMALL]; +const C_OBJECT_LARGE = intToChar[I_OBJECT_LARGE]; +const C_ARRAY_SMALL = intToChar[I_ARRAY_SMALL]; +const C_ARRAY_LARGE = intToChar[I_ARRAY_LARGE]; +const C_SET_SMALL = intToChar[I_SET_SMALL]; +const C_SET_LARGE = intToChar[I_SET_LARGE]; +const C_MAP_SMALL = intToChar[I_MAP_SMALL]; +const C_MAP_LARGE = intToChar[I_MAP_LARGE]; +const C_ARRAYBUFFER = intToChar[I_ARRAYBUFFER]; +const C_INT8ARRAY = intToChar[I_INT8ARRAY]; +const C_UINT8ARRAY = intToChar[I_UINT8ARRAY]; +const C_UINT8CLAMPEDARRAY = intToChar[I_UINT8CLAMPEDARRAY]; +const C_INT16ARRAY = intToChar[I_INT16ARRAY]; +const C_UINT16ARRAY = intToChar[I_UINT16ARRAY]; +const C_INT32ARRAY = intToChar[I_INT32ARRAY]; +const C_UINT32ARRAY = intToChar[I_UINT32ARRAY]; +const C_FLOAT32ARRAY = intToChar[I_FLOAT32ARRAY]; +const C_FLOAT64ARRAY = intToChar[I_FLOAT64ARRAY]; +const C_DATAVIEW = intToChar[I_DATAVIEW]; + +// Just reuse already defined constants, we just need distinct values +const I_STRING = I_STRING_SMALL; +const I_NUMBER = I_FLOAT; +const I_BOOL = I_BOOL_FALSE; +const I_OBJECT = I_OBJECT_SMALL; +const I_ARRAY = I_ARRAY_SMALL; +const I_SET = I_SET_SMALL; +const I_MAP = I_MAP_SMALL; + +const typeToSerializedInt = { + 'string': I_STRING, + 'number': I_NUMBER, + 'boolean': I_BOOL, + 'object': I_OBJECT, +}; + +const xtypeToSerializedInt = { + '[object RegExp]': I_REGEXP, + '[object Date]': I_DATE, + '[object Array]': I_ARRAY, + '[object Set]': I_SET, + '[object Map]': I_MAP, + '[object ArrayBuffer]': I_ARRAYBUFFER, + '[object Int8Array]': I_INT8ARRAY, + '[object Uint8Array]': I_UINT8ARRAY, + '[object Uint8ClampedArray]': I_UINT8CLAMPEDARRAY, + '[object Int16Array]': I_INT16ARRAY, + '[object Uint16Array]': I_UINT16ARRAY, + '[object Int32Array]': I_INT32ARRAY, + '[object Uint32Array]': I_UINT32ARRAY, + '[object Float32Array]': I_FLOAT32ARRAY, + '[object Float64Array]': I_FLOAT64ARRAY, + '[object DataView]': I_DATAVIEW, +}; + +const xtypeToSerializedChar = { + '[object Int8Array]': C_INT8ARRAY, + '[object Uint8Array]': C_UINT8ARRAY, + '[object Uint8ClampedArray]': C_UINT8CLAMPEDARRAY, + '[object Int16Array]': C_INT16ARRAY, + '[object Uint16Array]': C_UINT16ARRAY, + '[object Int32Array]': C_INT32ARRAY, + '[object Uint32Array]': C_UINT32ARRAY, + '[object Float32Array]': C_FLOAT32ARRAY, + '[object Float64Array]': C_FLOAT64ARRAY, +}; + +const toArrayBufferViewConstructor = { + [`${I_INT8ARRAY}`]: Int8Array, + [`${I_UINT8ARRAY}`]: Uint8Array, + [`${I_UINT8CLAMPEDARRAY}`]: Uint8ClampedArray, + [`${I_INT16ARRAY}`]: Int16Array, + [`${I_UINT16ARRAY}`]: Uint16Array, + [`${I_INT32ARRAY}`]: Int32Array, + [`${I_UINT32ARRAY}`]: Uint32Array, + [`${I_FLOAT32ARRAY}`]: Float32Array, + [`${I_FLOAT64ARRAY}`]: Float64Array, + [`${I_DATAVIEW}`]: DataView, +}; + +/******************************************************************************/ + +const textDecoder = new TextDecoder(); +const textEncoder = new TextEncoder(); +const isInteger = Number.isInteger; + +const writeRefs = new Map(); +const writeBuffer = []; + +const readRefs = new Map(); +let readStr = ''; +let readPtr = 0; +let readEnd = 0; + +let refCounter = 1; + +let uint8Input = null; + +const uint8InputFromAsciiStr = s => { + if ( uint8Input === null || uint8Input.length < s.length ) { + uint8Input = new Uint8Array(s.length + 0x03FF & ~0x03FF); + } + textEncoder.encodeInto(s, uint8Input); + return uint8Input; +}; + +const isInstanceOf = (o, s) => { + return typeof o === 'object' && o !== null && ( + s === 'Object' || Object.prototype.toString.call(o) === `[object ${s}]` + ); +}; + +const shouldCompress = (s, options) => + options.compress === true && ( + options.compressThreshold === undefined || + options.compressThreshold <= s.length + ); + +/******************************************************************************* + * + * A large Uint is always a positive integer (can be zero), assumed to be + * large, i.e. > NUMSAFECHARS -- but not necessarily. The serialized value has + * always at least one digit, and is always followed by a separator. + * + * */ + +const strFromLargeUint = i => { + let r = 0, s = ''; + for (;;) { + r = i % NUMSAFECHARS; + s += intToChar[r]; + i -= r; + if ( i === 0 ) { break; } + i /= NUMSAFECHARS; + } + return s + SEPARATORCHAR; +}; + +const deserializeLargeUint = ( ) => { + let c = readStr.charCodeAt(readPtr++); + let n = charCodeToInt[c]; + let m = 1; + while ( (c = readStr.charCodeAt(readPtr++)) !== SEPARATORCHARCODE ) { + m *= NUMSAFECHARS; + n += m * charCodeToInt[c]; + } + return n; +}; + +/******************************************************************************* + * + * Methods specific to ArrayBuffer objects to serialize optimally according to + * the content of the buffer. + * + * In sparse mode, number of output bytes per input int32 (4-byte) value: + * [v === zero]: 1 byte (separator) + * [v !== zero]: n digits + 1 byte (separator) + * + * */ + +const sparseValueLen = v => v !== 0 + ? (Math.log2(v) / BITS_PER_SAFECHARS | 0) + 2 + : 1; + +const analyzeArrayBuffer = arrbuf => { + const byteLength = arrbuf.byteLength; + const uint32len = byteLength >>> 2; + const uint32arr = new Uint32Array(arrbuf, 0, uint32len); + let notzeroCount = 0; + for ( let i = uint32len-1; i >= 0; i-- ) { + if ( uint32arr[i] === 0 ) { continue; } + notzeroCount = i + 1; + break; + } + const end = notzeroCount + 1 <= uint32len ? notzeroCount << 2 : byteLength; + const endUint32 = end >>> 2; + const remUint8 = end & 0b11; + const denseSize = endUint32 * 5 + (remUint8 ? remUint8 + 1 : 0); + let sparseSize = 0; + for ( let i = 0; i < endUint32; i++ ) { + sparseSize += sparseValueLen(uint32arr[i]); + if ( sparseSize > denseSize ) { + return { end, dense: true, denseSize }; + } + } + if ( remUint8 !== 0 ) { + sparseSize += 1; // sentinel + const uint8arr = new Uint8Array(arrbuf, endUint32 << 2); + for ( let i = 0; i < remUint8; i++ ) { + sparseSize += sparseValueLen(uint8arr[i]); + } + } + return { end, dense: false, sparseSize }; +}; + +const denseArrayBufferToStr = (arrbuf, details) => { + const end = details.end; + const m = end % 4; + const n = end - m; + const uin32len = n >>> 2; + const uint32arr = new Uint32Array(arrbuf, 0, uin32len); + const output = new Uint8Array(details.denseSize); + let j = 0, v = 0; + for ( let i = 0; i < uin32len; i++ ) { + v = uint32arr[i]; + output[j+0] = intToCharCode[v % NUMSAFECHARS]; + v = v / NUMSAFECHARS | 0; + output[j+1] = intToCharCode[v % NUMSAFECHARS]; + v = v / NUMSAFECHARS | 0; + output[j+2] = intToCharCode[v % NUMSAFECHARS]; + v = v / NUMSAFECHARS | 0; + output[j+3] = intToCharCode[v % NUMSAFECHARS]; + v = v / NUMSAFECHARS | 0; + output[j+4] = intToCharCode[v]; + j += 5; + } + if ( m !== 0 ) { + const uint8arr = new Uint8Array(arrbuf, n); + v = uint8arr[0]; + if ( m > 1 ) { + v += uint8arr[1] << 8; + if ( m > 2 ) { + v += uint8arr[2] << 16; + } + } + output[j+0] = intToCharCode[v % NUMSAFECHARS]; + v = v / NUMSAFECHARS | 0; + output[j+1] = intToCharCode[v % NUMSAFECHARS]; + if ( m > 1 ) { + v = v / NUMSAFECHARS | 0; + output[j+2] = intToCharCode[v % NUMSAFECHARS]; + if ( m > 2 ) { + v = v / NUMSAFECHARS | 0; + output[j+3] = intToCharCode[v % NUMSAFECHARS]; + } + } + } + return textDecoder.decode(output); +}; + +const BASE88_POW1 = NUMSAFECHARS; +const BASE88_POW2 = NUMSAFECHARS * BASE88_POW1; +const BASE88_POW3 = NUMSAFECHARS * BASE88_POW2; +const BASE88_POW4 = NUMSAFECHARS * BASE88_POW3; + +const denseArrayBufferFromStr = (denseStr, arrbuf) => { + const input = uint8InputFromAsciiStr(denseStr); + const end = denseStr.length; + const m = end % 5; + const n = end - m; + const uin32len = n / 5 * 4 >>> 2; + const uint32arr = new Uint32Array(arrbuf, 0, uin32len); + let j = 0, v = 0; + for ( let i = 0; i < n; i += 5 ) { + v = charCodeToInt[input[i+0]]; + v += charCodeToInt[input[i+1]] * BASE88_POW1; + v += charCodeToInt[input[i+2]] * BASE88_POW2; + v += charCodeToInt[input[i+3]] * BASE88_POW3; + v += charCodeToInt[input[i+4]] * BASE88_POW4; + uint32arr[j++] = v; + } + if ( m === 0 ) { return; } + v = charCodeToInt[input[n+0]] + + charCodeToInt[input[n+1]] * BASE88_POW1; + if ( m > 2 ) { + v += charCodeToInt[input[n+2]] * BASE88_POW2; + if ( m > 3 ) { + v += charCodeToInt[input[n+3]] * BASE88_POW3; + } + } + const uint8arr = new Uint8Array(arrbuf, j << 2); + uint8arr[0] = v & 255; + if ( v !== 0 ) { + v >>>= 8; + uint8arr[1] = v & 255; + if ( v !== 0 ) { + v >>>= 8; + uint8arr[2] = v & 255; + } + } +}; + +const sparseArrayBufferToStr = (arrbuf, details) => { + const end = details.end; + const uint8out = new Uint8Array(details.sparseSize); + const uint32len = end >>> 2; + const uint32arr = new Uint32Array(arrbuf, 0, uint32len); + let j = 0, n = 0, r = 0; + for ( let i = 0; i < uint32len; i++ ) { + n = uint32arr[i]; + if ( n !== 0 ) { + for (;;) { + r = n % NUMSAFECHARS; + uint8out[j++] = intToCharCode[r]; + n -= r; + if ( n === 0 ) { break; } + n /= NUMSAFECHARS; + } + } + uint8out[j++] = SEPARATORCHARCODE; + } + const uint8rem = end & 0b11; + if ( uint8rem !== 0 ) { + uint8out[j++] = SENTINELCHARCODE; + const uint8arr = new Uint8Array(arrbuf, end - uint8rem, uint8rem); + for ( let i = 0; i < uint8rem; i++ ) { + n = uint8arr[i]; + if ( n !== 0 ) { + for (;;) { + r = n % NUMSAFECHARS; + uint8out[j++] = intToCharCode[r]; + n -= r; + if ( n === 0 ) { break; } + n /= NUMSAFECHARS; + } + } + uint8out[j++] = SEPARATORCHARCODE; + } + } + return textDecoder.decode(uint8out); +}; + +const sparseArrayBufferFromStr = (sparseStr, arrbuf) => { + const sparseLen = sparseStr.length; + const input = uint8InputFromAsciiStr(sparseStr); + const end = arrbuf.byteLength; + const uint32len = end >>> 2; + const uint32arr = new Uint32Array(arrbuf, 0, uint32len); + let i = 0, j = 0, c = 0, n = 0, m = 0; + for ( ; j < sparseLen; i++ ) { + c = input[j++]; + if ( c === SEPARATORCHARCODE ) { continue; } + if ( c === SENTINELCHARCODE ) { break; } + n = charCodeToInt[c]; + m = 1; + for (;;) { + c = input[j++]; + if ( c === SEPARATORCHARCODE ) { break; } + m *= NUMSAFECHARS; + n += m * charCodeToInt[c]; + } + uint32arr[i] = n; + } + if ( c === SENTINELCHARCODE ) { + i <<= 2; + const uint8arr = new Uint8Array(arrbuf, i); + for ( ; j < sparseLen; i++ ) { + c = input[j++]; + if ( c === SEPARATORCHARCODE ) { continue; } + n = charCodeToInt[c]; + m = 1; + for (;;) { + c = input[j++]; + if ( c === SEPARATORCHARCODE ) { break; } + m *= NUMSAFECHARS; + n += m * charCodeToInt[c]; + } + uint8arr[i] = n; + } + } +}; + +/******************************************************************************/ + +const _serialize = data => { + // Primitive types + if ( data === 0 ) { + writeBuffer.push(C_ZERO); + return; + } + if ( data === null ) { + writeBuffer.push(C_NULL); + return; + } + if ( data === undefined ) { + writeBuffer.push(C_UNDEFINED); + return; + } + // Type name + switch ( typeToSerializedInt[typeof data] ) { + case I_STRING: { + const length = data.length; + if ( length < NUMSAFECHARS ) { + writeBuffer.push(C_STRING_SMALL + intToChar[length], data); + } else { + writeBuffer.push(C_STRING_LARGE + strFromLargeUint(length), data); + } + return; + } + case I_NUMBER: + if ( isInteger(data) ) { + if ( data >= NUMSAFECHARS ) { + writeBuffer.push(C_INTEGER_LARGE_POS + strFromLargeUint(data)); + } else if ( data > 0 ) { + writeBuffer.push(C_INTEGER_SMALL_POS + intToChar[data]); + } else if ( data > -NUMSAFECHARS ) { + writeBuffer.push(C_INTEGER_SMALL_NEG + intToChar[-data]); + } else { + writeBuffer.push(C_INTEGER_LARGE_NEG + strFromLargeUint(-data)); + } + } else { + const s = `${data}`; + writeBuffer.push(C_FLOAT + strFromLargeUint(s.length) + s); + } + return; + case I_BOOL: + writeBuffer.push(data ? C_BOOL_TRUE : C_BOOL_FALSE); + return; + case I_OBJECT: + break; + default: + return; + } + const xtypeName = Object.prototype.toString.call(data); + const xtypeInt = xtypeToSerializedInt[xtypeName]; + if ( xtypeInt === I_REGEXP ) { + writeBuffer.push(C_REGEXP); + _serialize(data.source); + _serialize(data.flags); + return; + } + if ( xtypeInt === I_DATE ) { + writeBuffer.push(C_DATE + _serialize(data.getTime())); + return; + } + // Reference to composite types + const ref = writeRefs.get(data); + if ( ref !== undefined ) { + writeBuffer.push(C_REFERENCE + strFromLargeUint(ref)); + return; + } + // Remember reference + writeRefs.set(data, refCounter++); + // Extended type name + switch ( xtypeInt ) { + case I_ARRAY: { + const size = data.length; + if ( size < NUMSAFECHARS ) { + writeBuffer.push(C_ARRAY_SMALL + intToChar[size]); + } else { + writeBuffer.push(C_ARRAY_LARGE + strFromLargeUint(size)); + } + for ( const v of data ) { + _serialize(v); + } + return; + } + case I_SET: { + const size = data.size; + if ( size < NUMSAFECHARS ) { + writeBuffer.push(C_SET_SMALL + intToChar[size]); + } else { + writeBuffer.push(C_SET_LARGE + strFromLargeUint(size)); + } + for ( const v of data ) { + _serialize(v); + } + return; + } + case I_MAP: { + const size = data.size; + if ( size < NUMSAFECHARS ) { + writeBuffer.push(C_MAP_SMALL + intToChar[size]); + } else { + writeBuffer.push(C_MAP_LARGE + strFromLargeUint(size)); + } + for ( const [ k, v ] of data ) { + _serialize(k); + _serialize(v); + } + return; + } + case I_ARRAYBUFFER: { + const byteLength = data.byteLength; + writeBuffer.push(C_ARRAYBUFFER + strFromLargeUint(byteLength)); + _serialize(data.maxByteLength); + const arrbuffDetails = analyzeArrayBuffer(data); + _serialize(arrbuffDetails.dense); + const str = arrbuffDetails.dense + ? denseArrayBufferToStr(data, arrbuffDetails) + : sparseArrayBufferToStr(data, arrbuffDetails); + _serialize(str); + //console.log(`arrbuf size=${byteLength} content size=${arrbuffDetails.end} dense=${arrbuffDetails.dense} array size=${arrbuffDetails.dense ? arrbuffDetails.denseSize : arrbuffDetails.sparseSize} serialized size=${str.length}`); + return; + } + case I_INT8ARRAY: + case I_UINT8ARRAY: + case I_UINT8CLAMPEDARRAY: + case I_INT16ARRAY: + case I_UINT16ARRAY: + case I_INT32ARRAY: + case I_UINT32ARRAY: + case I_FLOAT32ARRAY: + case I_FLOAT64ARRAY: + writeBuffer.push( + xtypeToSerializedChar[xtypeName], + strFromLargeUint(data.byteOffset), + strFromLargeUint(data.length) + ); + _serialize(data.buffer); + return; + case I_DATAVIEW: + writeBuffer.push(C_DATAVIEW, strFromLargeUint(data.byteOffset), strFromLargeUint(data.byteLength)); + _serialize(data.buffer); + return; + default: { + const keys = Object.keys(data); + const size = keys.length; + if ( size < NUMSAFECHARS ) { + writeBuffer.push(C_OBJECT_SMALL + intToChar[size]); + } else { + writeBuffer.push(C_OBJECT_LARGE + strFromLargeUint(size)); + } + for ( const key of keys ) { + _serialize(key); + _serialize(data[key]); + } + break; + } + } +}; + +/******************************************************************************/ + +const _deserialize = ( ) => { + if ( readPtr >= readEnd ) { return; } + const type = charCodeToInt[readStr.charCodeAt(readPtr++)]; + switch ( type ) { + // Primitive types + case I_STRING_SMALL: + case I_STRING_LARGE: { + const size = type === I_STRING_SMALL + ? charCodeToInt[readStr.charCodeAt(readPtr++)] + : deserializeLargeUint(); + const beg = readPtr; + readPtr += size; + return readStr.slice(beg, readPtr); + } + case I_ZERO: + return 0; + case I_INTEGER_SMALL_POS: + return charCodeToInt[readStr.charCodeAt(readPtr++)]; + case I_INTEGER_SMALL_NEG: + return -charCodeToInt[readStr.charCodeAt(readPtr++)]; + case I_INTEGER_LARGE_POS: + return deserializeLargeUint(); + case I_INTEGER_LARGE_NEG: + return -deserializeLargeUint(); + case I_BOOL_FALSE: + return false; + case I_BOOL_TRUE: + return true; + case I_NULL: + return null; + case I_UNDEFINED: + return; + case I_FLOAT: { + const size = deserializeLargeUint(); + const beg = readPtr; + readPtr += size; + return parseFloat(readStr.slice(beg, readPtr)); + } + case I_REGEXP: { + const source = _deserialize(); + const flags = _deserialize(); + return new RegExp(source, flags); + } + case I_DATE: { + const time = _deserialize(); + return new Date(time); + } + case I_REFERENCE: { + const ref = deserializeLargeUint(); + return readRefs.get(ref); + } + case I_OBJECT_SMALL: + case I_OBJECT_LARGE: { + const entries = []; + const size = type === I_OBJECT_SMALL + ? charCodeToInt[readStr.charCodeAt(readPtr++)] + : deserializeLargeUint(); + for ( let i = 0; i < size; i++ ) { + const k = _deserialize(); + const v = _deserialize(); + entries.push([ k, v ]); + } + const out = Object.fromEntries(entries); + readRefs.set(refCounter++, out); + return out; + } + case I_ARRAY_SMALL: + case I_ARRAY_LARGE: { + const out = []; + const size = type === I_ARRAY_SMALL + ? charCodeToInt[readStr.charCodeAt(readPtr++)] + : deserializeLargeUint(); + for ( let i = 0; i < size; i++ ) { + out.push(_deserialize()); + } + readRefs.set(refCounter++, out); + return out; + } + case I_SET_SMALL: + case I_SET_LARGE: { + const entries = []; + const size = type === I_SET_SMALL + ? charCodeToInt[readStr.charCodeAt(readPtr++)] + : deserializeLargeUint(); + for ( let i = 0; i < size; i++ ) { + entries.push(_deserialize()); + } + const out = new Set(entries); + readRefs.set(refCounter++, out); + return out; + } + case I_MAP_SMALL: + case I_MAP_LARGE: { + const entries = []; + const size = type === I_MAP_SMALL + ? charCodeToInt[readStr.charCodeAt(readPtr++)] + : deserializeLargeUint(); + for ( let i = 0; i < size; i++ ) { + const k = _deserialize(); + const v = _deserialize(); + entries.push([ k, v ]); + } + const out = new Map(entries); + readRefs.set(refCounter++, out); + return out; + } + case I_ARRAYBUFFER: { + const byteLength = deserializeLargeUint(); + const maxByteLength = _deserialize(); + let options; + if ( maxByteLength !== 0 && maxByteLength !== byteLength ) { + options = { maxByteLength }; + } + const arrbuf = new ArrayBuffer(byteLength, options); + const dense = _deserialize(); + const str = _deserialize(); + if ( dense ) { + denseArrayBufferFromStr(str, arrbuf); + } else { + sparseArrayBufferFromStr(str, arrbuf); + } + readRefs.set(refCounter++, arrbuf); + return arrbuf; + } + case I_INT8ARRAY: + case I_UINT8ARRAY: + case I_UINT8CLAMPEDARRAY: + case I_INT16ARRAY: + case I_UINT16ARRAY: + case I_INT32ARRAY: + case I_UINT32ARRAY: + case I_FLOAT32ARRAY: + case I_FLOAT64ARRAY: + case I_DATAVIEW: { + const byteOffset = deserializeLargeUint(); + const length = deserializeLargeUint(); + const arrayBuffer = _deserialize(); + const ctor = toArrayBufferViewConstructor[`${type}`]; + const out = new ctor(arrayBuffer, byteOffset, length); + readRefs.set(refCounter++, out); + return out; + } + default: + break; + } + readPtr = FAILMARK; +}; + +/******************************************************************************* + * + * LZ4 block compression/decompression + * + * Imported from: + * https://github.com/gorhill/lz4-wasm/blob/8995cdef7b/dist/lz4-block-codec-js.js + * + * Customized to avoid external dependencies as I entertain the idea of + * spinning off the serializer as a standalone utility for all to use. + * + * */ + +class LZ4BlockJS { + constructor() { + this.hashTable = undefined; + this.outputBuffer = undefined; + } + reset() { + this.hashTable = undefined; + this.outputBuffer = undefined; + } + growOutputBuffer(size) { + if ( this.outputBuffer !== undefined ) { + if ( this.outputBuffer.byteLength >= size ) { return; } + } + this.outputBuffer = new ArrayBuffer(size + 0xFFFF & 0x7FFF0000); + } + encodeBound(size) { + return size > 0x7E000000 ? 0 : size + (size / 255 | 0) + 16; + } + encodeBlock(iBuf, oOffset) { + const iLen = iBuf.byteLength; + if ( iLen >= 0x7E000000 ) { throw new RangeError(); } + // "The last match must start at least 12 bytes before end of block" + const lastMatchPos = iLen - 12; + // "The last 5 bytes are always literals" + const lastLiteralPos = iLen - 5; + if ( this.hashTable === undefined ) { + this.hashTable = new Int32Array(65536); + } + this.hashTable.fill(-65536); + if ( isInstanceOf(iBuf, 'ArrayBuffer') ) { + iBuf = new Uint8Array(iBuf); + } + const oLen = oOffset + this.encodeBound(iLen); + this.growOutputBuffer(oLen); + const oBuf = new Uint8Array(this.outputBuffer, 0, oLen); + let iPos = 0; + let oPos = oOffset; + let anchorPos = 0; + // sequence-finding loop + for (;;) { + let refPos; + let mOffset; + let sequence = iBuf[iPos] << 8 | iBuf[iPos+1] << 16 | iBuf[iPos+2] << 24; + // match-finding loop + while ( iPos <= lastMatchPos ) { + sequence = sequence >>> 8 | iBuf[iPos+3] << 24; + const hash = (sequence * 0x9E37 & 0xFFFF) + (sequence * 0x79B1 >>> 16) & 0xFFFF; + refPos = this.hashTable[hash]; + this.hashTable[hash] = iPos; + mOffset = iPos - refPos; + if ( + mOffset < 65536 && + iBuf[refPos+0] === ((sequence ) & 0xFF) && + iBuf[refPos+1] === ((sequence >>> 8) & 0xFF) && + iBuf[refPos+2] === ((sequence >>> 16) & 0xFF) && + iBuf[refPos+3] === ((sequence >>> 24) & 0xFF) + ) { + break; + } + iPos += 1; + } + // no match found + if ( iPos > lastMatchPos ) { break; } + // match found + let lLen = iPos - anchorPos; + let mLen = iPos; + iPos += 4; refPos += 4; + while ( iPos < lastLiteralPos && iBuf[iPos] === iBuf[refPos] ) { + iPos += 1; refPos += 1; + } + mLen = iPos - mLen; + const token = mLen < 19 ? mLen - 4 : 15; + // write token, length of literals if needed + if ( lLen >= 15 ) { + oBuf[oPos++] = 0xF0 | token; + let l = lLen - 15; + while ( l >= 255 ) { + oBuf[oPos++] = 255; + l -= 255; + } + oBuf[oPos++] = l; + } else { + oBuf[oPos++] = (lLen << 4) | token; + } + // write literals + while ( lLen-- ) { + oBuf[oPos++] = iBuf[anchorPos++]; + } + if ( mLen === 0 ) { break; } + // write offset of match + oBuf[oPos+0] = mOffset; + oBuf[oPos+1] = mOffset >>> 8; + oPos += 2; + // write length of match if needed + if ( mLen >= 19 ) { + let l = mLen - 19; + while ( l >= 255 ) { + oBuf[oPos++] = 255; + l -= 255; + } + oBuf[oPos++] = l; + } + anchorPos = iPos; + } + // last sequence is literals only + let lLen = iLen - anchorPos; + if ( lLen >= 15 ) { + oBuf[oPos++] = 0xF0; + let l = lLen - 15; + while ( l >= 255 ) { + oBuf[oPos++] = 255; + l -= 255; + } + oBuf[oPos++] = l; + } else { + oBuf[oPos++] = lLen << 4; + } + while ( lLen-- ) { + oBuf[oPos++] = iBuf[anchorPos++]; + } + return new Uint8Array(oBuf.buffer, 0, oPos); + } + decodeBlock(iBuf, iOffset, oLen) { + const iLen = iBuf.byteLength; + this.growOutputBuffer(oLen); + const oBuf = new Uint8Array(this.outputBuffer, 0, oLen); + let iPos = iOffset, oPos = 0; + while ( iPos < iLen ) { + const token = iBuf[iPos++]; + // literals + let clen = token >>> 4; + // length of literals + if ( clen !== 0 ) { + if ( clen === 15 ) { + let l; + for (;;) { + l = iBuf[iPos++]; + if ( l !== 255 ) { break; } + clen += 255; + } + clen += l; + } + // copy literals + const end = iPos + clen; + while ( iPos < end ) { + oBuf[oPos++] = iBuf[iPos++]; + } + if ( iPos === iLen ) { break; } + } + // match + const mOffset = iBuf[iPos+0] | (iBuf[iPos+1] << 8); + if ( mOffset === 0 || mOffset > oPos ) { return; } + iPos += 2; + // length of match + clen = (token & 0x0F) + 4; + if ( clen === 19 ) { + let l; + for (;;) { + l = iBuf[iPos++]; + if ( l !== 255 ) { break; } + clen += 255; + } + clen += l; + } + // copy match + const end = oPos + clen; + let mPos = oPos - mOffset; + while ( oPos < end ) { + oBuf[oPos++] = oBuf[mPos++]; + } + } + return oBuf; + } + encode(input, outputOffset) { + if ( isInstanceOf(input, 'ArrayBuffer') ) { + input = new Uint8Array(input); + } else if ( isInstanceOf(input, 'Uint8Array') === false ) { + throw new TypeError(); + } + return this.encodeBlock(input, outputOffset); + } + decode(input, inputOffset, outputSize) { + if ( isInstanceOf(input, 'ArrayBuffer') ) { + input = new Uint8Array(input); + } else if ( isInstanceOf(input, 'Uint8Array') === false ) { + throw new TypeError(); + } + return this.decodeBlock(input, inputOffset, outputSize); + } +} + +/******************************************************************************* + * + * Synchronous APIs + * + * */ + +export const serialize = (data, options = {}) => { + refCounter = 1; + _serialize(data); + writeBuffer.unshift(MAGICPREFIX); + const s = writeBuffer.join(''); + writeRefs.clear(); + writeBuffer.length = 0; + if ( shouldCompress(s, options) === false ) { return s; } + const lz4Util = new LZ4BlockJS(); + const uint8ArrayBefore = textEncoder.encode(s); + const uint8ArrayAfter = lz4Util.encode(uint8ArrayBefore, 0); + const lz4 = { + size: uint8ArrayBefore.length, + data: new Uint8Array(uint8ArrayAfter), + }; + refCounter = 1; + _serialize(lz4); + writeBuffer.unshift(MAGICLZ4PREFIX); + const t = writeBuffer.join(''); + writeRefs.clear(); + writeBuffer.length = 0; + const ratio = t.length / s.length; + return ratio <= 0.85 ? t : s; +}; + +export const deserialize = s => { + if ( s.startsWith(MAGICLZ4PREFIX) ) { + refCounter = 1; + readStr = s; + readEnd = s.length; + readPtr = MAGICLZ4PREFIX.length; + const lz4 = _deserialize(); + readRefs.clear(); + readStr = ''; + const lz4Util = new LZ4BlockJS(); + const uint8ArrayAfter = lz4Util.decode(lz4.data, 0, lz4.size); + s = textDecoder.decode(new Uint8Array(uint8ArrayAfter)); + } + if ( s.startsWith(MAGICPREFIX) === false ) { return; } + refCounter = 1; + readStr = s; + readEnd = s.length; + readPtr = MAGICPREFIX.length; + const data = _deserialize(); + readRefs.clear(); + readStr = ''; + uint8Input = null; + if ( readPtr === FAILMARK ) { return; } + return data; +}; + +export const isSerialized = s => + typeof s === 'string' && + (s.startsWith(MAGICLZ4PREFIX) || s.startsWith(MAGICPREFIX)); + +export const isCompressed = s => + typeof s === 'string' && s.startsWith(MAGICLZ4PREFIX); + +/******************************************************************************* + * + * Configuration + * + * */ + +const defaultConfig = { + threadTTL: 3000, +}; + +const validateConfig = { + threadTTL: val => val > 0, +}; + +const currentConfig = Object.assign({}, defaultConfig); + +export const getConfig = ( ) => Object.assign({}, currentConfig); + +export const setConfig = config => { + for ( const key in Object.keys(config) ) { + if ( defaultConfig.hasOwnProperty(key) === false ) { continue; } + const val = config[key]; + if ( typeof val !== typeof defaultConfig[key] ) { continue; } + if ( (validateConfig[key])(val) === false ) { continue; } + currentConfig[key] = val; + } +}; + +/******************************************************************************* + * + * Asynchronous APIs + * + * Being asynchronous allows to support workers and future features such as + * checksums. + * + * */ + +const THREAD_AREYOUREADY = 1; +const THREAD_IAMREADY = 2; +const THREAD_SERIALIZE = 3; +const THREAD_DESERIALIZE = 4; + +class MainThread { + constructor() { + this.name = 'main'; + this.jobs = []; + this.workload = 0; + this.timer = undefined; + this.busy = 2; + } + + process() { + if ( this.jobs.length === 0 ) { return; } + const job = this.jobs.shift(); + this.workload -= job.size; + const result = job.what === THREAD_SERIALIZE + ? serialize(job.data, job.options) + : deserialize(job.data); + job.resolve(result); + this.processAsync(); + if ( this.jobs.length === 0 ) { + this.busy = 2; + } else if ( this.busy > 2 ) { + this.busy -= 1; + } + } + + processAsync() { + if ( this.timer !== undefined ) { return; } + if ( this.jobs.length === 0 ) { return; } + this.timer = globalThis.requestIdleCallback(deadline => { + this.timer = undefined; + globalThis.queueMicrotask(( ) => { + this.process(); + }); + if ( deadline.timeRemaining() === 0 ) { + this.busy += 1; + } + }, { timeout: 5 }); + } + + serialize(data, options) { + return new Promise(resolve => { + this.workload += 1; + this.jobs.push({ what: THREAD_SERIALIZE, data, options, size: 1, resolve }); + this.processAsync(); + }); + } + + deserialize(data, options) { + return new Promise(resolve => { + const size = data.length; + this.workload += size; + this.jobs.push({ what: THREAD_DESERIALIZE, data, options, size, resolve }); + this.processAsync(); + }); + } + + get queueSize() { + return this.jobs.length; + } + + get workSize() { + return this.workload * this.busy; + } +} + +class Thread { + constructor(gcer) { + this.name = 'worker'; + this.jobs = new Map(); + this.jobIdGenerator = 1; + this.workload = 0; + this.workerAccessTime = 0; + this.workerTimer = undefined; + this.gcer = gcer; + this.workerPromise = new Promise(resolve => { + let worker = null; + try { + worker = new Worker('js/s14e-serializer.js', { type: 'module' }); + worker.onmessage = ev => { + const msg = ev.data; + if ( isInstanceOf(msg, 'Object') === false ) { return; } + if ( msg.what === THREAD_IAMREADY ) { + worker.onmessage = ev => { this.onmessage(ev); }; + worker.onerror = null; + resolve(worker); + } + }; + worker.onerror = ( ) => { + worker.onmessage = worker.onerror = null; + resolve(null); + }; + worker.postMessage({ + what: THREAD_AREYOUREADY, + config: currentConfig, + }); + } catch(ex) { + console.info(ex); + worker.onmessage = worker.onerror = null; + resolve(null); + } + }); + } + + countdownWorker() { + if ( this.workerTimer !== undefined ) { return; } + this.workerTimer = setTimeout(async ( ) => { + this.workerTimer = undefined; + if ( this.jobs.size !== 0 ) { return; } + const idleTime = Date.now() - this.workerAccessTime; + if ( idleTime < currentConfig.threadTTL ) { + return this.countdownWorker(); + } + const worker = await this.workerPromise; + if ( this.jobs.size !== 0 ) { return; } + this.gcer(this); + if ( worker === null ) { return; } + worker.onmessage = worker.onerror = null; + worker.terminate(); + }, currentConfig.threadTTL); + } + + onmessage(ev) { + this.ondone(ev.data); + } + + ondone(job) { + const resolve = this.jobs.get(job.id); + if ( resolve === undefined ) { return; } + this.jobs.delete(job.id); + resolve(job.result); + this.workload -= job.size; + if ( this.jobs.size !== 0 ) { return; } + this.countdownWorker(); + } + + async serialize(data, options) { + return new Promise(resolve => { + const id = this.jobIdGenerator++; + this.workload += 1; + this.jobs.set(id, resolve); + return this.workerPromise.then(worker => { + this.workerAccessTime = Date.now(); + if ( worker === null ) { + this.ondone({ id, result: serialize(data, options), size: 1 }); + } else { + worker.postMessage({ what: THREAD_SERIALIZE, id, data, options, size: 1 }); + } + }); + }); + } + + async deserialize(data, options) { + return new Promise(resolve => { + const id = this.jobIdGenerator++; + const size = data.length; + this.workload += size; + this.jobs.set(id, resolve); + return this.workerPromise.then(worker => { + this.workerAccessTime = Date.now(); + if ( worker === null ) { + this.ondone({ id, result: deserialize(data, options), size }); + } else { + worker.postMessage({ what: THREAD_DESERIALIZE, id, data, options, size }); + } + }); + }); + } + + get queueSize() { + return this.jobs.size; + } + + get workSize() { + return this.workload; + } +} + +const threads = { + pool: [ new MainThread() ], + thread(maxPoolSize) { + const poolSize = this.pool.length; + if ( poolSize !== 0 && poolSize >= maxPoolSize ) { + if ( poolSize === 1 ) { return this.pool[0]; } + return this.pool.reduce((a, b) => { + //console.log(`${a.name}: q=${a.queueSize} w=${a.workSize} ${b.name}: q=${b.queueSize} w=${b.workSize}`); + if ( b.queueSize === 0 ) { return b; } + if ( a.queueSize === 0 ) { return a; } + return b.workSize < a.workSize ? b : a; + }); + } + const thread = new Thread(thread => { + const pos = this.pool.indexOf(thread); + if ( pos === -1 ) { return; } + this.pool.splice(pos, 1); + }); + this.pool.push(thread); + return thread; + }, +}; + +export async function serializeAsync(data, options = {}) { + const maxThreadCount = options.multithreaded || 0; + if ( maxThreadCount === 0 ) { + return serialize(data, options); + } + const thread = threads.thread(maxThreadCount); + //console.log(`serializeAsync: thread=${thread.name} workload=${thread.workSize}`); + const result = await thread.serialize(data, options); + if ( result !== undefined ) { return result; } + return serialize(data, options); +} + +export async function deserializeAsync(data, options = {}) { + if ( isSerialized(data) === false ) { return data; } + const maxThreadCount = options.multithreaded || 0; + if ( maxThreadCount === 0 ) { + return deserialize(data, options); + } + const thread = threads.thread(maxThreadCount); + //console.log(`deserializeAsync: thread=${thread.name} data=${data.length} workload=${thread.workSize}`); + const result = await thread.deserialize(data, options); + if ( result !== undefined ) { return result; } + return deserialize(data, options); +} + +/******************************************************************************* + * + * Worker-only code + * + * */ + +if ( isInstanceOf(globalThis, 'DedicatedWorkerGlobalScope') ) { + globalThis.onmessage = ev => { + const msg = ev.data; + switch ( msg.what ) { + case THREAD_AREYOUREADY: + setConfig(msg.config); + globalThis.postMessage({ what: THREAD_IAMREADY }); + break; + case THREAD_SERIALIZE: + const result = serialize(msg.data, msg.options); + globalThis.postMessage({ id: msg.id, size: msg.size, result }); + break; + case THREAD_DESERIALIZE: { + const result = deserialize(msg.data); + globalThis.postMessage({ id: msg.id, size: msg.size, result }); + break; + } + } + }; +} + +/******************************************************************************/ diff --git a/src/js/scriptlet-filtering-core.js b/src/js/scriptlet-filtering-core.js index 125eb87..907844f 100644 --- a/src/js/scriptlet-filtering-core.js +++ b/src/js/scriptlet-filtering-core.js @@ -98,10 +98,18 @@ const patchScriptlet = (content, arglist) => { ); }; +const requote = s => { + if ( /^(["'`]).+\1$|,/.test(s) === false ) { return s; } + if ( s.includes("'") === false ) { return `'${s}'`; } + if ( s.includes('"') === false ) { return `"${s}"`; } + if ( s.includes('`') === false ) { return `\`${s}\``; } + return `'${s.replace(/'/g, "\\'")}'`; +}; + const decompile = json => { - const args = JSON.parse(json).map(s => s.replace(/,/g, '\\,')); + const args = JSON.parse(json); if ( args.length === 0 ) { return '+js()'; } - return `+js(${args.join(', ')})`; + return `+js(${args.map(s => requote(s)).join(', ')})`; }; /******************************************************************************/ @@ -192,7 +200,7 @@ export class ScriptletFilteringEngine { } fromSelfie(selfie) { - if ( selfie instanceof Object === false ) { return false; } + if ( typeof selfie !== 'object' || selfie === null ) { return false; } if ( selfie.version !== VERSION ) { return false; } this.scriptletDB.fromSelfie(selfie); return true; @@ -251,16 +259,10 @@ export class ScriptletFilteringEngine { $mainWorldMap.clear(); $isolatedWorldMap.clear(); - if ( scriptletDetails.mainWorld === '' ) { - if ( scriptletDetails.isolatedWorld === '' ) { - return { filters: scriptletDetails.filters }; - } - } - - const scriptletGlobals = options.scriptletGlobals || []; + const scriptletGlobals = options.scriptletGlobals || {}; if ( options.debug ) { - scriptletGlobals.push([ 'canDebug', true ]); + scriptletGlobals.canDebug = true; } return { @@ -271,7 +273,7 @@ export class ScriptletFilteringEngine { options.debugScriptlets ? 'debugger;' : ';', '', // For use by scriptlets to share local data among themselves - `const scriptletGlobals = new Map(${JSON.stringify(scriptletGlobals, null, 2)});`, + `const scriptletGlobals = ${JSON.stringify(scriptletGlobals, null, 4)}`, '', scriptletDetails.mainWorld, '', @@ -285,7 +287,7 @@ export class ScriptletFilteringEngine { options.debugScriptlets ? 'debugger;' : ';', '', // For use by scriptlets to share local data among themselves - `const scriptletGlobals = new Map(${JSON.stringify(scriptletGlobals, null, 2)});`, + `const scriptletGlobals = ${JSON.stringify(scriptletGlobals, null, 4)}`, '', scriptletDetails.isolatedWorld, '', diff --git a/src/js/scriptlet-filtering.js b/src/js/scriptlet-filtering.js index 10da19f..7da840d 100644 --- a/src/js/scriptlet-filtering.js +++ b/src/js/scriptlet-filtering.js @@ -44,13 +44,6 @@ import { const contentScriptRegisterer = new (class { constructor() { this.hostnameToDetails = new Map(); - if ( browser.contentScripts === undefined ) { return; } - onBroadcast(msg => { - if ( msg.what !== 'filteringBehaviorChanged' ) { return; } - if ( msg.direction > 0 ) { return; } - if ( msg.hostname ) { return this.flush(msg.hostname); } - this.reset(); - }); } register(hostname, code) { if ( browser.contentScripts === undefined ) { return false; } @@ -78,6 +71,7 @@ const contentScriptRegisterer = new (class { return false; } unregister(hostname) { + if ( hostname === '' ) { return; } if ( this.hostnameToDetails.size === 0 ) { return; } const details = this.hostnameToDetails.get(hostname); if ( details === undefined ) { return; } @@ -85,6 +79,7 @@ const contentScriptRegisterer = new (class { this.unregisterHandle(details.handle); } flush(hostname) { + if ( hostname === '' ) { return; } if ( hostname === '*' ) { return this.reset(); } for ( const hn of this.hostnameToDetails.keys() ) { if ( hn.endsWith(hostname) === false ) { continue; } @@ -128,16 +123,15 @@ const mainWorldInjector = (( ) => { 'json-slot', ');', ]; + const jsonSlot = parts.indexOf('json-slot'); return { - parts, - jsonSlot: parts.indexOf('json-slot'), - assemble: function(hostname, scriptlets, filters) { - this.parts[this.jsonSlot] = JSON.stringify({ + assemble: function(hostname, details) { + parts[jsonSlot] = JSON.stringify({ hostname, - scriptlets, - filters, + scriptlets: details.mainWorld, + filters: details.filters, }); - return this.parts.join(''); + return parts.join(''); }, }; })(); @@ -160,24 +154,61 @@ const isolatedWorldInjector = (( ) => { 'json-slot', ');', ]; + const jsonSlot = parts.indexOf('json-slot'); return { - parts, - jsonSlot: parts.indexOf('json-slot'), - assemble: function(hostname, scriptlets) { - this.parts[this.jsonSlot] = JSON.stringify({ hostname }); - const code = this.parts.join(''); + assemble(hostname, details) { + parts[jsonSlot] = JSON.stringify({ hostname }); + const code = parts.join(''); // Manually substitute noop function with scriptlet wrapper // function, so as to not suffer instances of special // replacement characters `$`,`\` when using String.replace() // with scriptlet code. const match = /function\(\)\{\}/.exec(code); return code.slice(0, match.index) + - scriptlets + + details.isolatedWorld + code.slice(match.index + match[0].length); }, }; })(); +const onScriptletMessageInjector = (( ) => { + const parts = [ + '(', + function(name) { + if ( self.uBO_bcSecret ) { return; } + const bcSecret = new self.BroadcastChannel(name); + bcSecret.onmessage = ev => { + const msg = ev.data; + switch ( typeof msg ) { + case 'string': + if ( msg !== 'areyouready?' ) { break; } + bcSecret.postMessage('iamready!'); + break; + case 'object': + if ( self.vAPI && self.vAPI.messaging ) { + self.vAPI.messaging.send('contentscript', msg); + } else { + console.log(`[uBO][${msg.type}]${msg.text}`); + } + break; + } + }; + bcSecret.postMessage('iamready!'); + self.uBO_bcSecret = bcSecret; + }.toString(), + ')(', + 'bcSecret-slot', + ');', + ]; + const bcSecretSlot = parts.indexOf('bcSecret-slot'); + return { + assemble(details) { + parts[bcSecretSlot] = JSON.stringify(details.bcSecret); + return parts.join('\n'); + }, + }; +})(); + /******************************************************************************/ export class ScriptletFilteringEngineEx extends ScriptletFilteringEngine { @@ -187,10 +218,44 @@ export class ScriptletFilteringEngineEx extends ScriptletFilteringEngine { this.warSecret = undefined; this.scriptletCache = new MRUCache(32); this.isDevBuild = undefined; - onBroadcast(msg => { - if ( msg.what !== 'hiddenSettingsChanged' ) { return; } - this.scriptletCache.reset(); - this.isDevBuild = undefined; + this.logLevel = 1; + this.bc = onBroadcast(msg => { + switch ( msg.what ) { + case 'filteringBehaviorChanged': { + const direction = msg.direction || 0; + if ( direction > 0 ) { return; } + if ( direction >= 0 && msg.hostname ) { + return contentScriptRegisterer.flush(msg.hostname); + } + contentScriptRegisterer.reset(); + break; + } + case 'hiddenSettingsChanged': + this.isDevBuild = undefined; + /* fall through */ + case 'loggerEnabled': + case 'loggerDisabled': + this.clearCache(); + break; + case 'loggerLevelChanged': + this.logLevel = msg.level; + vAPI.tabs.query({ + discarded: false, + url: [ 'http://*/*', 'https://*/*' ], + }).then(tabs => { + for ( const tab of tabs ) { + const { status } = tab; + if ( status !== 'loading' && status !== 'complete' ) { continue; } + vAPI.tabs.executeScript(tab.id, { + allFrames: true, + file: `/js/scriptlets/scriptlet-loglevel-${this.logLevel}.js`, + matchAboutBlank: true, + }); + } + }); + this.clearCache(); + break; + } }); } @@ -208,6 +273,11 @@ export class ScriptletFilteringEngineEx extends ScriptletFilteringEngine { contentScriptRegisterer.reset(); } + clearCache() { + this.scriptletCache.reset(); + contentScriptRegisterer.reset(); + } + retrieve(request) { const { hostname } = request; @@ -238,58 +308,85 @@ export class ScriptletFilteringEngineEx extends ScriptletFilteringEngine { this.warSecret = vAPI.warSecret.long(); } + const bcSecret = vAPI.generateSecret(3); + const options = { - scriptletGlobals: [ - [ 'warOrigin', this.warOrigin ], - [ 'warSecret', this.warSecret ], - ], + scriptletGlobals: { + warOrigin: this.warOrigin, + warSecret: this.warSecret, + }, debug: this.isDevBuild, debugScriptlets: µb.hiddenSettings.debugScriptlets, }; + if ( logger.enabled ) { + options.scriptletGlobals.bcSecret = bcSecret; + options.scriptletGlobals.logLevel = this.logLevel; + } scriptletDetails = super.retrieve(request, options); - this.scriptletCache.add(hostname, scriptletDetails || null); + if ( scriptletDetails === undefined ) { + if ( request.nocache !== true ) { + this.scriptletCache.add(hostname, null); + } + return; + } + + const contentScript = []; + if ( scriptletDetails.mainWorld ) { + contentScript.push(mainWorldInjector.assemble(hostname, scriptletDetails)); + } + if ( scriptletDetails.isolatedWorld ) { + contentScript.push(isolatedWorldInjector.assemble(hostname, scriptletDetails)); + } + + const cachedScriptletDetails = { + bcSecret, + code: contentScript.join('\n\n'), + filters: scriptletDetails.filters, + }; - return scriptletDetails; + if ( request.nocache !== true ) { + this.scriptletCache.add(hostname, cachedScriptletDetails); + } + + return cachedScriptletDetails; } injectNow(details) { if ( typeof details.frameId !== 'number' ) { return; } - const request = { + const hostname = hostnameFromURI(details.url); + const domain = domainFromHostname(hostname); + + const scriptletDetails = this.retrieve({ tabId: details.tabId, frameId: details.frameId, url: details.url, - hostname: hostnameFromURI(details.url), - domain: undefined, - entity: undefined - }; - - request.domain = domainFromHostname(request.hostname); - request.entity = entityFromDomain(request.domain); - - const scriptletDetails = this.retrieve(request); + hostname, + domain, + entity: entityFromDomain(domain), + }); if ( scriptletDetails === undefined ) { - contentScriptRegisterer.unregister(request.hostname); + contentScriptRegisterer.unregister(hostname); return; } - - const contentScript = []; - if ( µb.hiddenSettings.debugScriptletInjector ) { - contentScript.push('debugger'); + if ( Boolean(scriptletDetails.code) === false ) { + return scriptletDetails; } - const { mainWorld = '', isolatedWorld = '', filters } = scriptletDetails; - if ( mainWorld !== '' ) { - contentScript.push(mainWorldInjector.assemble(request.hostname, mainWorld, filters)); + + const contentScript = [ scriptletDetails.code ]; + if ( logger.enabled ) { + contentScript.unshift( + onScriptletMessageInjector.assemble(scriptletDetails) + ); } - if ( isolatedWorld !== '' ) { - contentScript.push(isolatedWorldInjector.assemble(request.hostname, isolatedWorld)); + if ( µb.hiddenSettings.debugScriptletInjector ) { + contentScript.unshift('debugger'); } - const code = contentScript.join('\n\n'); - const isAlreadyInjected = contentScriptRegisterer.register(request.hostname, code); + const isAlreadyInjected = contentScriptRegisterer.register(hostname, code); if ( isAlreadyInjected !== true ) { vAPI.tabs.executeScript(details.tabId, { code, @@ -298,7 +395,6 @@ export class ScriptletFilteringEngineEx extends ScriptletFilteringEngine { runAt: 'document_start', }); } - return scriptletDetails; } diff --git a/src/js/scriptlets/epicker.js b/src/js/scriptlets/epicker.js index 80489e8..41b0b76 100644 --- a/src/js/scriptlets/epicker.js +++ b/src/js/scriptlets/epicker.js @@ -619,6 +619,21 @@ const filterToDOMInterface = (( ) => { const reCaret = '(?:[^%.0-9a-z_-]|$)'; const rePseudoElements = /:(?::?after|:?before|:[a-z-]+)$/; + const matchElemToRegex = (elem, re) => { + const srcProp = netFilter1stSources[elem.localName]; + let src = elem[srcProp]; + if ( src instanceof SVGAnimatedString ) { + src = src.baseVal; + } + if ( typeof src === 'string' && /^https?:\/\//.test(src) ) { + if ( re.test(src) ) { return srcProp; } + } + src = elem.currentSrc; + if ( typeof src === 'string' && /^https?:\/\//.test(src) ) { + if ( re.test(src) ) { return srcProp; } + } + }; + // Net filters: we need to lookup manually -- translating into a foolproof // CSS selector is just not possible. // @@ -672,28 +687,21 @@ const filterToDOMInterface = (( ) => { // Lookup by tag names. // https://github.com/uBlockOrigin/uBlock-issues/issues/2260 // Maybe get to the actual URL indirectly. + // + // https://github.com/uBlockOrigin/uBlock-issues/issues/3142 + // Don't try to match against non-network URIs. const elems = document.querySelectorAll( Object.keys(netFilter1stSources).join() ); for ( const elem of elems ) { - const srcProp = netFilter1stSources[elem.localName]; - let src = elem[srcProp]; - if ( src instanceof SVGAnimatedString ) { - src = src.baseVal; - } - if ( - typeof src === 'string' && - reFilter.test(src) || - typeof elem.currentSrc === 'string' && - reFilter.test(elem.currentSrc) - ) { - out.push({ - elem, - src: srcProp, - opt: filterTypes[elem.localName], - style: vAPI.hideStyle, - }); - } + const srcProp = matchElemToRegex(elem, reFilter); + if ( srcProp === undefined ) { continue; } + out.push({ + elem, + src: srcProp, + opt: filterTypes[elem.localName], + style: vAPI.hideStyle, + }); } // Find matching background image in current set of candidate elements. @@ -1247,6 +1255,7 @@ const pickerCSSStyle = [ 'display: block', 'filter: none', 'height: 100vh', + ' height: 100svh', 'left: 0', 'margin: 0', 'max-height: none', diff --git a/src/js/scriptlets/scriptlet-loglevel-1.js b/src/js/scriptlets/scriptlet-loglevel-1.js new file mode 100644 index 0000000..bc5f4bb --- /dev/null +++ b/src/js/scriptlets/scriptlet-loglevel-1.js @@ -0,0 +1,49 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2024-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +'use strict'; + +/******************************************************************************/ + +(( ) => { + if ( self.uBO_bcSecret instanceof self.BroadcastChannel === false ) { return; } + self.uBO_bcSecret.postMessage('setScriptletLogLevelToOne'); +})(); + + + + + + + + +/******************************************************************************* + + DO NOT: + - Remove the following code + - Add code beyond the following code + Reason: + - https://github.com/gorhill/uBlock/pull/3721 + - uBO never uses the return value from injected content scripts + +**/ + +void 0; diff --git a/src/js/scriptlets/scriptlet-loglevel-2.js b/src/js/scriptlets/scriptlet-loglevel-2.js new file mode 100644 index 0000000..d8afefd --- /dev/null +++ b/src/js/scriptlets/scriptlet-loglevel-2.js @@ -0,0 +1,49 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2024-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +'use strict'; + +/******************************************************************************/ + +(( ) => { + if ( self.uBO_bcSecret instanceof self.BroadcastChannel === false ) { return; } + self.uBO_bcSecret.postMessage('setScriptletLogLevelToTwo'); +})(); + + + + + + + + +/******************************************************************************* + + DO NOT: + - Remove the following code + - Add code beyond the following code + Reason: + - https://github.com/gorhill/uBlock/pull/3721 + - uBO never uses the return value from injected content scripts + +**/ + +void 0; diff --git a/src/js/scriptlets/should-inject-contentscript.js b/src/js/scriptlets/should-inject-contentscript.js index b9a2658..94d0cd3 100644 --- a/src/js/scriptlets/should-inject-contentscript.js +++ b/src/js/scriptlets/should-inject-contentscript.js @@ -29,7 +29,7 @@ (( ) => { try { - let status = vAPI.uBO !== true; + const status = vAPI.uBO !== true; if ( status === false && vAPI.bootstrap ) { self.requestIdleCallback(( ) => vAPI && vAPI.bootstrap()); } diff --git a/src/js/settings.js b/src/js/settings.js index deb033f..fc0ea68 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -27,7 +27,7 @@ import { setAccentColor, setTheme } from './theme.js'; /******************************************************************************/ -const handleImportFilePicker = function() { +function handleImportFilePicker() { const file = this.files[0]; if ( file === undefined || file.name === '' ) { return; } @@ -88,22 +88,22 @@ const handleImportFilePicker = function() { }; fr.readAsText(file); -}; +} /******************************************************************************/ -const startImportFilePicker = function() { +function startImportFilePicker() { const input = qs$('#restoreFilePicker'); // Reset to empty string, this will ensure an change event is properly // triggered if the user pick a file, even if it is the same as the last // one picked. input.value = ''; input.click(); -}; +} /******************************************************************************/ -const exportToFile = async function() { +async function exportToFile() { const response = await vAPI.messaging.send('dashboard', { what: 'backupUserData', }); @@ -119,11 +119,11 @@ const exportToFile = async function() { 'filename': response.localData.lastBackupFile }); onLocalDataReceived(response.localData); -}; +} /******************************************************************************/ -const onLocalDataReceived = function(details) { +function onLocalDataReceived(details) { let v, unit; if ( typeof details.storageUsed === 'number' ) { v = details.storageUsed; @@ -187,32 +187,32 @@ const onLocalDataReceived = function(details) { dom.attr('[data-setting-name="hyperlinkAuditingDisabled"]', 'disabled', ''); dom.attr('[data-setting-name="webrtcIPAddressHidden"]', 'disabled', ''); } -}; +} /******************************************************************************/ -const resetUserData = function() { +function resetUserData() { const msg = i18n$('aboutResetDataConfirm'); const proceed = window.confirm(msg); if ( proceed !== true ) { return; } vAPI.messaging.send('dashboard', { what: 'resetUserData', }); -}; +} /******************************************************************************/ -const synchronizeDOM = function() { +function synchronizeDOM() { dom.cl.toggle( dom.body, 'advancedUser', qs$('[data-setting-name="advancedUserEnabled"]').checked === true ); -}; +} /******************************************************************************/ -const changeUserSettings = function(name, value) { +function changeUserSettings(name, value) { vAPI.messaging.send('dashboard', { what: 'userSettings', name, @@ -235,11 +235,11 @@ const changeUserSettings = function(name, value) { default: break; } -}; +} /******************************************************************************/ -const onValueChanged = function(ev) { +function onValueChanged(ev) { const input = ev.target; const name = dom.attr(input, 'data-setting-name'); let value = input.value; @@ -256,14 +256,20 @@ const onValueChanged = function(ev) { } changeUserSettings(name, value); -}; +} /******************************************************************************/ // TODO: use data-* to declare simple settings -const onUserSettingsReceived = function(details) { +function onUserSettingsReceived(details) { const checkboxes = qsa$('[data-setting-type="bool"]'); + const onchange = ev => { + const checkbox = ev.target; + const name = checkbox.dataset.settingName || ''; + changeUserSettings(name, checkbox.checked); + synchronizeDOM(); + }; for ( const checkbox of checkboxes ) { const name = dom.attr(checkbox, 'data-setting-name') || ''; if ( details[name] === undefined ) { @@ -272,10 +278,7 @@ const onUserSettingsReceived = function(details) { continue; } checkbox.checked = details[name] === true; - dom.on(checkbox, 'change', ( ) => { - changeUserSettings(name, checkbox.checked); - synchronizeDOM(); - }); + dom.on(checkbox, 'change', onchange); } if ( details.canLeakLocalIPAddresses === true ) { @@ -295,6 +298,14 @@ const onUserSettingsReceived = function(details) { dom.on('#restoreFilePicker', 'change', handleImportFilePicker); synchronizeDOM(); +} + +/******************************************************************************/ + +self.wikilink = 'https://github.com/gorhill/uBlock/wiki/Dashboard:-Settings'; + +self.hasUnsavedData = function() { + return false; }; /******************************************************************************/ diff --git a/src/js/start.js b/src/js/start.js index 5762619..46a052f 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -63,6 +63,11 @@ import { /******************************************************************************/ +let lastVersionInt = 0; +let thisVersionInt = 0; + +/******************************************************************************/ + vAPI.app.onShutdown = ( ) => { staticFilteringReverseLookup.shutdown(); io.updateStop(); @@ -76,6 +81,10 @@ vAPI.app.onShutdown = ( ) => { permanentSwitches.reset(); }; +vAPI.alarms.onAlarm.addListener(alarm => { + µb.alarmQueue.push(alarm.name); +}); + /******************************************************************************/ // This is called only once, when everything has been loaded in memory after @@ -139,22 +148,29 @@ const initializeTabs = async ( ) => { // https://www.reddit.com/r/uBlockOrigin/comments/s7c9go/ // Abort suspending network requests when uBO is merely being installed. -const onVersionReady = lastVersion => { - if ( lastVersion === vAPI.app.version ) { return; } +const onVersionReady = async lastVersion => { + lastVersionInt = vAPI.app.intFromVersion(lastVersion); + thisVersionInt = vAPI.app.intFromVersion(vAPI.app.version); + if ( thisVersionInt === lastVersionInt ) { return; } vAPI.storage.set({ version: vAPI.app.version, versionUpdateTime: Date.now(), }); - const lastVersionInt = vAPI.app.intFromVersion(lastVersion); - // Special case: first installation if ( lastVersionInt === 0 ) { vAPI.net.unsuspend({ all: true, discard: true }); return; } + // Remove cache items with obsolete names + if ( lastVersionInt < vAPI.app.intFromVersion('1.56.1b5') ) { + io.remove(`compiled/${µb.pslAssetKey}`); + io.remove('compiled/redirectEngine/resources'); + io.remove('selfie/main'); + } + // Since built-in resources may have changed since last version, we // force a reload of all resources. redirectEngine.invalidateResourcesSelfie(io); @@ -162,11 +178,6 @@ const onVersionReady = lastVersion => { /******************************************************************************/ -// https://github.com/chrisaljoudi/uBlock/issues/226 -// Whitelist in memory. -// Whitelist parser needs PSL to be ready. -// gorhill 2014-12-15: not anymore -// // https://github.com/uBlockOrigin/uBlock-issues/issues/1433 // Allow admins to add their own trusted-site directives. @@ -174,16 +185,38 @@ const onNetWhitelistReady = (netWhitelistRaw, adminExtra) => { if ( typeof netWhitelistRaw === 'string' ) { netWhitelistRaw = netWhitelistRaw.split('\n'); } + + // Remove now obsolete built-in trusted directives + if ( lastVersionInt !== thisVersionInt ) { + if ( lastVersionInt < vAPI.app.intFromVersion('1.56.1b12') ) { + const obsolete = [ + 'about-scheme', + 'chrome-scheme', + 'edge-scheme', + 'opera-scheme', + 'vivaldi-scheme', + 'wyciwyg-scheme', + ]; + for ( const directive of obsolete ) { + const i = netWhitelistRaw.findIndex(s => + s === directive || s === `# ${directive}` + ); + if ( i === -1 ) { continue; } + netWhitelistRaw.splice(i, 1); + } + } + } + // Append admin-controlled trusted-site directives - if ( - adminExtra instanceof Object && - Array.isArray(adminExtra.trustedSiteDirectives) - ) { - for ( const directive of adminExtra.trustedSiteDirectives ) { - µb.netWhitelistDefault.push(directive); - netWhitelistRaw.push(directive); + if ( adminExtra instanceof Object ) { + if ( Array.isArray(adminExtra.trustedSiteDirectives) ) { + for ( const directive of adminExtra.trustedSiteDirectives ) { + µb.netWhitelistDefault.push(directive); + netWhitelistRaw.push(directive); + } } } + µb.netWhitelist = µb.whitelistFromArray(netWhitelistRaw); µb.netWhitelistModifyTime = Date.now(); }; @@ -221,8 +254,7 @@ const onUserSettingsReady = fetched => { fetched.importedLists.length === 0 && fetched.externalLists !== '' ) { - fetched.importedLists = - fetched.externalLists.trim().split(/[\n\r]+/); + fetched.importedLists = fetched.externalLists.trim().split(/[\n\r]+/); } fromFetch(µb.userSettings, fetched); @@ -252,19 +284,19 @@ const onUserSettingsReady = fetched => { // Wait for removal of invalid cached data to be completed. const onCacheSettingsReady = async (fetched = {}) => { + let selfieIsInvalid = false; if ( fetched.compiledMagic !== µb.systemSettings.compiledMagic ) { µb.compiledFormatChanged = true; - µb.selfieIsInvalid = true; + selfieIsInvalid = true; ubolog(`Serialized format of static filter lists changed`); } if ( fetched.selfieMagic !== µb.systemSettings.selfieMagic ) { - µb.selfieIsInvalid = true; + selfieIsInvalid = true; ubolog(`Serialized format of selfie changed`); } - if ( µb.selfieIsInvalid ) { - µb.selfieManager.destroy(); - cacheStorage.set(µb.systemSettings); - } + if ( selfieIsInvalid === false ) { return; } + µb.selfieManager.destroy({ janitor: true }); + cacheStorage.set(µb.systemSettings); }; /******************************************************************************/ @@ -303,12 +335,6 @@ const onHiddenSettingsReady = async ( ) => { ubolog(`WASM modules ready ${Date.now()-vAPI.T0} ms after launch`); }); } - - // Maybe override default cache storage - µb.supportStats.cacheBackend = await cacheStorage.select( - µb.hiddenSettings.cacheStorageAPI - ); - ubolog(`Backend storage for cache will be ${µb.supportStats.cacheBackend}`); }; /******************************************************************************/ @@ -322,7 +348,6 @@ const onFirstFetchReady = (fetched, adminExtra) => { } // Order is important -- do not change: - fromFetch(µb.localSettings, fetched); fromFetch(µb.restoreBackupSettings, fetched); permanentFirewall.fromString(fetched.dynamicFilteringString); @@ -333,7 +358,6 @@ const onFirstFetchReady = (fetched, adminExtra) => { sessionSwitches.assign(permanentSwitches); onNetWhitelistReady(fetched.netWhitelist, adminExtra); - onVersionReady(fetched.version); }; /******************************************************************************/ @@ -358,14 +382,9 @@ const createDefaultProps = ( ) => { 'dynamicFilteringString': µb.dynamicFilteringDefault.join('\n'), 'urlFilteringString': '', 'hostnameSwitchesString': µb.hostnameSwitchesDefault.join('\n'), - 'lastRestoreFile': '', - 'lastRestoreTime': 0, - 'lastBackupFile': '', - 'lastBackupTime': 0, 'netWhitelist': µb.netWhitelistDefault, 'version': '0.0.0.0' }; - toFetch(µb.localSettings, fetchableProps); toFetch(µb.restoreBackupSettings, fetchableProps); return fetchableProps; }; @@ -389,23 +408,25 @@ try { const adminExtra = await vAPI.adminStorage.get('toAdd'); ubolog(`Extra admin settings ready ${Date.now()-vAPI.T0} ms after launch`); - // https://github.com/uBlockOrigin/uBlock-issues/issues/1365 - // Wait for onCacheSettingsReady() to be fully ready. - const [ , , lastVersion ] = await Promise.all([ + // Maybe override default cache storage + µb.supportStats.cacheBackend = await cacheStorage.select( + µb.hiddenSettings.cacheStorageAPI + ); + ubolog(`Backend storage for cache will be ${µb.supportStats.cacheBackend}`); + + await vAPI.storage.get(createDefaultProps()).then(async fetched => { + ubolog(`Version ready ${Date.now()-vAPI.T0} ms after launch`); + await onVersionReady(fetched.version); + return fetched; + }).then(fetched => { + ubolog(`First fetch ready ${Date.now()-vAPI.T0} ms after launch`); + onFirstFetchReady(fetched, adminExtra); + }); + + await Promise.all([ µb.loadSelectedFilterLists().then(( ) => { ubolog(`List selection ready ${Date.now()-vAPI.T0} ms after launch`); }), - cacheStorage.get( - { compiledMagic: 0, selfieMagic: 0 } - ).then(fetched => { - ubolog(`Cache magic numbers ready ${Date.now()-vAPI.T0} ms after launch`); - onCacheSettingsReady(fetched); - }), - vAPI.storage.get(createDefaultProps()).then(fetched => { - ubolog(`First fetch ready ${Date.now()-vAPI.T0} ms after launch`); - onFirstFetchReady(fetched, adminExtra); - return fetched.version; - }), µb.loadUserSettings().then(fetched => { ubolog(`User settings ready ${Date.now()-vAPI.T0} ms after launch`); onUserSettingsReady(fetched); @@ -413,10 +434,15 @@ try { µb.loadPublicSuffixList().then(( ) => { ubolog(`PSL ready ${Date.now()-vAPI.T0} ms after launch`); }), + cacheStorage.get({ compiledMagic: 0, selfieMagic: 0 }).then(bin => { + ubolog(`Cache magic numbers ready ${Date.now()-vAPI.T0} ms after launch`); + onCacheSettingsReady(bin); + }), + µb.loadLocalSettings(), ]); // https://github.com/uBlockOrigin/uBlock-issues/issues/1547 - if ( lastVersion === '0.0.0.0' && vAPI.webextFlavor.soup.has('chromium') ) { + if ( lastVersionInt === 0 && vAPI.webextFlavor.soup.has('chromium') ) { vAPI.app.restart(); return; } @@ -434,7 +460,7 @@ let selfieIsValid = false; try { selfieIsValid = await µb.selfieManager.load(); if ( selfieIsValid === true ) { - ubolog(`Selfie ready ${Date.now()-vAPI.T0} ms after launch`); + ubolog(`Loaded filtering engine from selfie ${Date.now()-vAPI.T0} ms after launch`); } } catch (ex) { console.trace(ex); @@ -471,15 +497,6 @@ webRequest.start(); // as possible ensure minimal memory usage baseline. lz4Codec.relinquish(); -// https://github.com/chrisaljoudi/uBlock/issues/184 -// Check for updates not too far in the future. -io.addObserver(µb.assetObserver.bind(µb)); -µb.scheduleAssetUpdater({ - updateDelay: µb.userSettings.autoUpdate - ? µb.hiddenSettings.autoUpdateDelayAfterLaunch * 1000 - : 0 -}); - // Force an update of the context menu according to the currently // active tab. contextMenu.update(); @@ -504,5 +521,47 @@ ubolog(`All ready ${µb.supportStats.allReadyAfter} after launch`); µb.isReadyResolve(); + +// https://github.com/chrisaljoudi/uBlock/issues/184 +// Check for updates not too far in the future. +io.addObserver(µb.assetObserver.bind(µb)); +if ( µb.userSettings.autoUpdate ) { + let needEmergencyUpdate = false; + const entries = await io.getUpdateAges({ + filters: µb.selectedFilterLists, + internal: [ '*' ], + }); + for ( const entry of entries ) { + if ( entry.ageNormalized < 2 ) { continue; } + needEmergencyUpdate = true; + break; + } + const updateDelay = needEmergencyUpdate + ? 2000 + : µb.hiddenSettings.autoUpdateDelayAfterLaunch * 1000; + µb.scheduleAssetUpdater({ + auto: true, + updateDelay, + fetchDelay: needEmergencyUpdate ? 1000 : undefined + }); +} + +// Process alarm queue +while ( µb.alarmQueue.length !== 0 ) { + const what = µb.alarmQueue.shift(); + ubolog(`Processing alarm event from suspended state: '${what}'`); + switch ( what ) { + case 'assetUpdater': + µb.scheduleAssetUpdater({ auto: true, updateDelay: 2000, fetchDelay : 1000 }); + break; + case 'createSelfie': + µb.selfieManager.create(); + break; + case 'saveLocalSettings': + µb.saveLocalSettings(); + break; + } +} + // <<<<< end of async/await scope })(); diff --git a/src/js/static-dnr-filtering.js b/src/js/static-dnr-filtering.js index fb677ad..ca66b86 100644 --- a/src/js/static-dnr-filtering.js +++ b/src/js/static-dnr-filtering.js @@ -299,10 +299,10 @@ function addToDNR(context, list) { if ( parser.isComment() ) { if ( line === `!#trusted on ${context.secret}` ) { - parser.trustedSource = true; + parser.options.trustedSource = true; context.trustedSource = true; } else if ( line === `!#trusted off ${context.secret}` ) { - parser.trustedSource = false; + parser.options.trustedSource = false; context.trustedSource = false; } continue; @@ -312,6 +312,8 @@ function addToDNR(context, list) { if ( parser.hasError() ) { if ( parser.astError === sfp.AST_ERROR_OPTION_EXCLUDED ) { context.invalid.add(`Incompatible with DNR: ${line}`); + } else { + context.invalid.add(`Rejected filter: ${line}`); } continue; } diff --git a/src/js/static-ext-filtering-db.js b/src/js/static-ext-filtering-db.js index 64a9c8d..e669c1e 100644 --- a/src/js/static-ext-filtering-db.js +++ b/src/js/static-ext-filtering-db.js @@ -141,8 +141,8 @@ const StaticExtFilteringHostnameDB = class { toSelfie() { return { version: this.version, - hostnameToSlotIdMap: Array.from(this.hostnameToSlotIdMap), - regexToSlotIdMap: Array.from(this.regexToSlotIdMap), + hostnameToSlotIdMap: this.hostnameToSlotIdMap, + regexToSlotIdMap: this.regexToSlotIdMap, hostnameSlots: this.hostnameSlots, strSlots: this.strSlots, size: this.size @@ -150,11 +150,11 @@ const StaticExtFilteringHostnameDB = class { } fromSelfie(selfie) { - if ( selfie === undefined ) { return; } - this.hostnameToSlotIdMap = new Map(selfie.hostnameToSlotIdMap); + if ( typeof selfie !== 'object' || selfie === null ) { return; } + this.hostnameToSlotIdMap = selfie.hostnameToSlotIdMap; // Regex-based lookup available in uBO 1.47.0 and above - if ( Array.isArray(selfie.regexToSlotIdMap) ) { - this.regexToSlotIdMap = new Map(selfie.regexToSlotIdMap); + if ( selfie.regexToSlotIdMap ) { + this.regexToSlotIdMap = selfie.regexToSlotIdMap; } this.hostnameSlots = selfie.hostnameSlots; this.strSlots = selfie.strSlots; diff --git a/src/js/static-ext-filtering.js b/src/js/static-ext-filtering.js index 8a2905e..e616e63 100644 --- a/src/js/static-ext-filtering.js +++ b/src/js/static-ext-filtering.js @@ -26,9 +26,8 @@ import cosmeticFilteringEngine from './cosmetic-filtering.js'; import htmlFilteringEngine from './html-filtering.js'; import httpheaderFilteringEngine from './httpheader-filtering.js'; -import io from './assets.js'; -import logger from './logger.js'; import scriptletFilteringEngine from './scriptlet-filtering.js'; +import logger from './logger.js'; /******************************************************************************* @@ -147,34 +146,24 @@ staticExtFilteringEngine.fromCompiledContent = function(reader, options) { htmlFilteringEngine.fromCompiledContent(reader, options); }; -staticExtFilteringEngine.toSelfie = function(path) { - return io.put( - `${path}/main`, - JSON.stringify({ - cosmetic: cosmeticFilteringEngine.toSelfie(), - scriptlets: scriptletFilteringEngine.toSelfie(), - httpHeaders: httpheaderFilteringEngine.toSelfie(), - html: htmlFilteringEngine.toSelfie(), - }) - ); +staticExtFilteringEngine.toSelfie = function() { + return { + cosmetic: cosmeticFilteringEngine.toSelfie(), + scriptlets: scriptletFilteringEngine.toSelfie(), + httpHeaders: httpheaderFilteringEngine.toSelfie(), + html: htmlFilteringEngine.toSelfie(), + }; }; -staticExtFilteringEngine.fromSelfie = function(path) { - return io.get(`${path}/main`).then(details => { - let selfie; - try { - selfie = JSON.parse(details.content); - } catch (ex) { - } - if ( selfie instanceof Object === false ) { return false; } - cosmeticFilteringEngine.fromSelfie(selfie.cosmetic); - httpheaderFilteringEngine.fromSelfie(selfie.httpHeaders); - htmlFilteringEngine.fromSelfie(selfie.html); - if ( scriptletFilteringEngine.fromSelfie(selfie.scriptlets) === false ) { - return false; - } - return true; - }); +staticExtFilteringEngine.fromSelfie = async function(selfie) { + if ( typeof selfie !== 'object' || selfie === null ) { return false; } + cosmeticFilteringEngine.fromSelfie(selfie.cosmetic); + httpheaderFilteringEngine.fromSelfie(selfie.httpHeaders); + htmlFilteringEngine.fromSelfie(selfie.html); + if ( scriptletFilteringEngine.fromSelfie(selfie.scriptlets) === false ) { + return false; + } + return true; }; /******************************************************************************/ diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index eb8988b..48c5f62 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -896,7 +896,8 @@ export class AstFilterParser { this.reResponseheaderPattern = /^\^responseheader\(.*\)$/; this.rePatternScriptletJsonArgs = /^\{.*\}$/; this.reGoodRegexToken = /[^\x01%0-9A-Za-z][%0-9A-Za-z]{7,}|[^\x01%0-9A-Za-z][%0-9A-Za-z]{1,6}[^\x01%0-9A-Za-z]/; - this.reBadCSP = /(?:=|;)\s*report-(?:to|uri)\b/; + this.reBadCSP = /(?:^|[;,])\s*report-(?:to|uri)\b/i; + this.reBadPP = /(?:^|[;,])\s*report-to\b/i; this.reNoopOption = /^_+$/; this.scriptletArgListParser = new ArgListParser(','); } @@ -1298,6 +1299,7 @@ export class AstFilterParser { let modifierType = 0; let requestTypeCount = 0; let unredirectableTypeCount = 0; + let badfilter = false; for ( let i = 0, n = this.nodeTypeRegisterPtr; i < n; i++ ) { const type = this.nodeTypeRegister[i]; const targetNode = this.nodeTypeLookupTable[type]; @@ -1321,6 +1323,8 @@ export class AstFilterParser { realBad = hasValue; break; case NODE_TYPE_NET_OPTION_NAME_BADFILTER: + badfilter = true; + /* falls through */ case NODE_TYPE_NET_OPTION_NAME_NOOP: realBad = isNegated || hasValue; break; @@ -1400,7 +1404,11 @@ export class AstFilterParser { realBad = this.isRegexPattern() === false; break; case NODE_TYPE_NET_OPTION_NAME_PERMISSIONS: - realBad = modifierType !== 0 || (hasValue || isException) === false; + realBad = modifierType !== 0 || + (hasValue || isException) === false || + this.reBadPP.test( + this.getNetOptionValue(NODE_TYPE_NET_OPTION_NAME_PERMISSIONS) + ); if ( realBad ) { break; } modifierType = type; break; @@ -1457,6 +1465,9 @@ export class AstFilterParser { this.addFlags(AST_FLAG_HAS_ERROR); } } + const requiresTrustedSource = ( ) => + this.options.trustedSource !== true && + isException === false && badfilter === false; switch ( modifierType ) { case NODE_TYPE_NET_OPTION_NAME_CNAME: realBad = abstractTypeCount || behaviorTypeCount || requestTypeCount; @@ -1484,7 +1495,7 @@ export class AstFilterParser { case NODE_TYPE_NET_OPTION_NAME_REPLACE: { realBad = abstractTypeCount || behaviorTypeCount || unredirectableTypeCount; if ( realBad ) { break; } - if ( isException !== true && this.options.trustedSource !== true ) { + if ( requiresTrustedSource() ) { this.astError = AST_ERROR_UNTRUSTED_SOURCE; realBad = true; break; @@ -1496,20 +1507,21 @@ export class AstFilterParser { } break; } - case NODE_TYPE_NET_OPTION_NAME_URLTRANSFORM: + case NODE_TYPE_NET_OPTION_NAME_URLTRANSFORM: { realBad = abstractTypeCount || behaviorTypeCount || unredirectableTypeCount; if ( realBad ) { break; } - if ( isException !== true && this.options.trustedSource !== true ) { + if ( requiresTrustedSource() ) { this.astError = AST_ERROR_UNTRUSTED_SOURCE; realBad = true; break; } const value = this.getNetOptionValue(NODE_TYPE_NET_OPTION_NAME_URLTRANSFORM); - if ( parseReplaceValue(value) === undefined ) { + if ( value !== '' && parseReplaceValue(value) === undefined ) { this.astError = AST_ERROR_OPTION_BADVALUE; realBad = true; } break; + } case NODE_TYPE_NET_OPTION_NAME_REMOVEPARAM: realBad = abstractTypeCount || behaviorTypeCount; break; @@ -3112,7 +3124,7 @@ class ExtSelectorCompiler { // context. const cssIdentifier = '[A-Za-z_][\\w-]*'; const cssClassOrId = `[.#]${cssIdentifier}`; - const cssAttribute = `\\[${cssIdentifier}(?:[*^$]?="[^"\\]\\\\]+")?\\]`; + const cssAttribute = `\\[${cssIdentifier}(?:[*^$]?="[^"\\]\\\\\\x09-\\x0D]+")?\\]`; const cssSimple = '(?:' + `${cssIdentifier}(?:${cssClassOrId})*(?:${cssAttribute})*` + '|' + @@ -3196,6 +3208,7 @@ class ExtSelectorCompiler { 'matches-path', 'min-text-length', 'others', + 'shadow', 'upward', 'watch-attr', 'xpath', @@ -3297,10 +3310,9 @@ class ExtSelectorCompiler { if ( this.astHasType(parts, 'Error') ) { return; } if ( this.astHasType(parts, 'Selector') === false ) { return; } if ( this.astIsValidSelectorList(parts) === false ) { return; } - if ( - this.astHasType(parts, 'ProceduralSelector') === false && - this.astHasType(parts, 'ActionSelector') === false - ) { + if ( this.astHasType(parts, 'ProceduralSelector') ) { + if ( this.astHasType(parts, 'PseudoElementSelector') ) { return; } + } else if ( this.astHasType(parts, 'ActionSelector') === false ) { return this.astSerialize(parts); } const r = this.astCompile(parts); @@ -3453,6 +3465,8 @@ class ExtSelectorCompiler { // https://github.com/uBlockOrigin/uBlock-issues/issues/2300 // Unquoted attribute values are parsed as Identifier instead of String. + // https://github.com/uBlockOrigin/uBlock-issues/issues/3127 + // Escape [\t\n\v\f\r] astSerializePart(part) { const out = []; const { data } = part; @@ -3468,7 +3482,14 @@ class ExtSelectorCompiler { if ( typeof value !== 'string' ) { value = data.value.name; } - value = value.replace(/["\\]/g, '\\$&'); + if ( /["\\]/.test(value) ) { + value = value.replace(/["\\]/g, '\\$&'); + } + if ( /[\x09-\x0D]/.test(value) ) { + value = value.replace(/[\x09-\x0D]/g, s => + `\\${s.charCodeAt(0).toString(16).toUpperCase()} ` + ); + } let flags = ''; if ( typeof data.flags === 'string' ) { if ( /^(is?|si?)$/.test(data.flags) === false ) { return; } @@ -3842,6 +3863,8 @@ class ExtSelectorCompiler { return this.compileText(arg); case 'remove-class': return this.compileText(arg); + case 'shadow': + return this.compileSelector(arg); case 'style': return this.compileStyleProperties(arg); case 'upward': @@ -3979,6 +4002,10 @@ class ExtSelectorCompiler { compileUpwardArgument(s) { const i = this.compileInteger(s, 1, 256); if ( i !== undefined ) { return i; } + return this.compilePlainSelector(s); + } + + compilePlainSelector(s) { const parts = this.astFromRaw(s, 'selectorList' ); if ( this.astIsValidSelectorList(parts) !== true ) { return; } if ( this.astHasType(parts, 'ProceduralSelector') ) { return; } @@ -4023,6 +4050,7 @@ class ExtSelectorCompiler { compileXpathExpression(s) { const r = this.unquoteString(s); if ( r.i !== s.length ) { return; } + if ( globalThis.document instanceof Object === false ) { return r.s; } try { globalThis.document.createExpression(r.s, null); } catch (e) { diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index d1e9a70..9a252fd 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -28,7 +28,6 @@ import { queueTask, dropTask } from './tasks.js'; import BidiTrieContainer from './biditrie.js'; import HNTrieContainer from './hntrie.js'; -import { sparseBase64 } from './base64-custom.js'; import { CompiledListReader } from './static-filtering-io.js'; import * as sfp from './static-filtering-parser.js'; @@ -493,17 +492,13 @@ const filterDataReset = ( ) => { filterData.fill(0); filterDataWritePtr = 2; }; -const filterDataToSelfie = ( ) => { - return JSON.stringify(Array.from(filterData.subarray(0, filterDataWritePtr))); -}; +const filterDataToSelfie = ( ) => + filterData.subarray(0, filterDataWritePtr); + const filterDataFromSelfie = selfie => { - if ( typeof selfie !== 'string' || selfie === '' ) { return false; } - const data = JSON.parse(selfie); - if ( Array.isArray(data) === false ) { return false; } - filterDataGrow(data.length); - filterDataWritePtr = data.length; - filterData.set(data); - filterDataShrink(); + if ( selfie instanceof Int32Array === false ) { return false; } + filterData = selfie; + filterDataWritePtr = selfie.length; return true; }; @@ -519,53 +514,15 @@ const filterRefsReset = ( ) => { filterRefs.fill(null); filterRefsWritePtr = 1; }; -const filterRefsToSelfie = ( ) => { - const refs = []; - for ( let i = 0; i < filterRefsWritePtr; i++ ) { - const v = filterRefs[i]; - if ( v instanceof RegExp ) { - refs.push({ t: 1, s: v.source, f: v.flags }); - continue; - } - if ( Array.isArray(v) ) { - refs.push({ t: 2, v }); - continue; - } - if ( typeof v !== 'object' || v === null ) { - refs.push({ t: 0, v }); - continue; - } - const out = Object.create(null); - for ( const prop of Object.keys(v) ) { - const value = v[prop]; - out[prop] = prop.startsWith('$') - ? (typeof value === 'string' ? '' : null) - : value; - } - refs.push({ t: 3, v: out }); - } - return JSON.stringify(refs); -}; +const filterRefsToSelfie = ( ) => + filterRefs.slice(0, filterRefsWritePtr); + const filterRefsFromSelfie = selfie => { - if ( typeof selfie !== 'string' || selfie === '' ) { return false; } - const refs = JSON.parse(selfie); - if ( Array.isArray(refs) === false ) { return false; } - for ( let i = 0; i < refs.length; i++ ) { - const v = refs[i]; - switch ( v.t ) { - case 0: - case 2: - case 3: - filterRefs[i] = v.v; - break; - case 1: - filterRefs[i] = new RegExp(v.s, v.f); - break; - default: - throw new Error('Unknown filter reference!'); - } + if ( Array.isArray(selfie) === false ) { return false; } + for ( let i = 0, n = selfie.length; i < n; i++ ) { + filterRefs[i] = selfie[i]; } - filterRefsWritePtr = refs.length; + filterRefsWritePtr = selfie.length; return true; }; @@ -3121,14 +3078,11 @@ const urlTokenizer = new (class { } toSelfie() { - return sparseBase64.encode( - this.knownTokens.buffer, - this.knownTokens.byteLength - ); + return this.knownTokens; } fromSelfie(selfie) { - return sparseBase64.decode(selfie, this.knownTokens.buffer); + this.knownTokens = selfie; } // https://github.com/chrisaljoudi/uBlock/issues/1118 @@ -4095,7 +4049,7 @@ FilterCompiler.prototype.FILTER_UNSUPPORTED = 2; /******************************************************************************/ /******************************************************************************/ -const FilterContainer = function() { +const StaticNetFilteringEngine = function() { this.compilerVersion = '10'; this.selfieVersion = '10'; @@ -4113,7 +4067,7 @@ const FilterContainer = function() { /******************************************************************************/ -FilterContainer.prototype.prime = function() { +StaticNetFilteringEngine.prototype.prime = function() { origHNTrieContainer.reset( keyvalStore.getItem('SNFE.origHNTrieContainer.trieDetails') ); @@ -4125,7 +4079,7 @@ FilterContainer.prototype.prime = function() { /******************************************************************************/ -FilterContainer.prototype.reset = function() { +StaticNetFilteringEngine.prototype.reset = function() { this.processedFilterCount = 0; this.acceptedCount = 0; this.discardedCount = 0; @@ -4159,7 +4113,7 @@ FilterContainer.prototype.reset = function() { /******************************************************************************/ -FilterContainer.prototype.freeze = function() { +StaticNetFilteringEngine.prototype.freeze = function() { const unserialize = CompiledListReader.unserialize; for ( const line of this.goodFilters ) { @@ -4256,7 +4210,7 @@ FilterContainer.prototype.freeze = function() { /******************************************************************************/ -FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) { +StaticNetFilteringEngine.prototype.dnrFromCompiled = function(op, context, ...args) { if ( op === 'begin' ) { Object.assign(context, { good: new Set(), @@ -4571,17 +4525,7 @@ FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) { } break; case 'uritransform': { - const path = rule.__modifierValue; - let priority = rule.priority || 1; - if ( rule.__modifierAction !== ALLOW_REALM ) { - const transform = { path }; - rule.action.type = 'redirect'; - rule.action.redirect = { transform }; - rule.priority = priority + 1; - } else { - rule.action.type = 'block'; - rule.priority = priority + 2; - } + dnrAddRuleError(rule, `Incompatible with DNR: uritransform=${rule.__modifierValue}`); break; } default: @@ -4601,7 +4545,7 @@ FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) { /******************************************************************************/ -FilterContainer.prototype.addFilterUnit = function( +StaticNetFilteringEngine.prototype.addFilterUnit = function( bits, tokenHash, inewunit @@ -4628,7 +4572,7 @@ FilterContainer.prototype.addFilterUnit = function( /******************************************************************************/ -FilterContainer.prototype.optimize = function(throttle = 0) { +StaticNetFilteringEngine.prototype.optimize = function(throttle = 0) { if ( this.optimizeTaskId !== undefined ) { dropTask(this.optimizeTaskId); this.optimizeTaskId = undefined; @@ -4684,55 +4628,28 @@ FilterContainer.prototype.optimize = function(throttle = 0) { /******************************************************************************/ -FilterContainer.prototype.toSelfie = async function(storage, path) { - if ( typeof storage !== 'object' || storage === null ) { return; } - if ( typeof storage.put !== 'function' ) { return; } - +StaticNetFilteringEngine.prototype.toSelfie = function() { + this.optimize(0); bidiTrieOptimize(true); - keyvalStore.setItem( - 'SNFE.origHNTrieContainer.trieDetails', + keyvalStore.setItem('SNFE.origHNTrieContainer.trieDetails', origHNTrieContainer.optimize() ); - - return Promise.all([ - storage.put( - `${path}/destHNTrieContainer`, - destHNTrieContainer.serialize(sparseBase64) - ), - storage.put( - `${path}/origHNTrieContainer`, - origHNTrieContainer.serialize(sparseBase64) - ), - storage.put( - `${path}/bidiTrie`, - bidiTrie.serialize(sparseBase64) - ), - storage.put( - `${path}/filterData`, - filterDataToSelfie() - ), - storage.put( - `${path}/filterRefs`, - filterRefsToSelfie() - ), - storage.put( - `${path}/main`, - JSON.stringify({ - version: this.selfieVersion, - processedFilterCount: this.processedFilterCount, - acceptedCount: this.acceptedCount, - discardedCount: this.discardedCount, - bitsToBucket: Array.from(this.bitsToBucket).map(kv => { - kv[1] = Array.from(kv[1]); - return kv; - }), - urlTokenizer: urlTokenizer.toSelfie(), - }) - ) - ]); + return { + version: this.selfieVersion, + processedFilterCount: this.processedFilterCount, + acceptedCount: this.acceptedCount, + discardedCount: this.discardedCount, + bitsToBucket: this.bitsToBucket, + urlTokenizer: urlTokenizer.toSelfie(), + destHNTrieContainer: destHNTrieContainer.toSelfie(), + origHNTrieContainer: origHNTrieContainer.toSelfie(), + bidiTrie: bidiTrie.toSelfie(), + filterData: filterDataToSelfie(), + filterRefs: filterRefsToSelfie(), + }; }; -FilterContainer.prototype.serialize = async function() { +StaticNetFilteringEngine.prototype.serialize = async function() { const selfie = []; const storage = { put(name, data) { @@ -4745,53 +4662,27 @@ FilterContainer.prototype.serialize = async function() { /******************************************************************************/ -FilterContainer.prototype.fromSelfie = async function(storage, path) { - if ( typeof storage !== 'object' || storage === null ) { return; } - if ( typeof storage.get !== 'function' ) { return; } +StaticNetFilteringEngine.prototype.fromSelfie = async function(selfie) { + if ( typeof selfie !== 'object' || selfie === null ) { return; } this.reset(); this.notReady = true; - const results = await Promise.all([ - storage.get(`${path}/main`), - storage.get(`${path}/destHNTrieContainer`).then(details => - destHNTrieContainer.unserialize(details.content, sparseBase64) - ), - storage.get(`${path}/origHNTrieContainer`).then(details => - origHNTrieContainer.unserialize(details.content, sparseBase64) - ), - storage.get(`${path}/bidiTrie`).then(details => - bidiTrie.unserialize(details.content, sparseBase64) - ), - storage.get(`${path}/filterData`).then(details => - filterDataFromSelfie(details.content) - ), - storage.get(`${path}/filterRefs`).then(details => - filterRefsFromSelfie(details.content) - ), - ]); - + const results = [ + destHNTrieContainer.fromSelfie(selfie.destHNTrieContainer), + origHNTrieContainer.fromSelfie(selfie.origHNTrieContainer), + bidiTrie.fromSelfie(selfie.bidiTrie), + filterDataFromSelfie(selfie.filterData), + filterRefsFromSelfie(selfie.filterRefs), + ]; if ( results.slice(1).every(v => v === true) === false ) { return false; } - const details = results[0]; - if ( typeof details !== 'object' || details === null ) { return false; } - if ( typeof details.content !== 'string' ) { return false; } - if ( details.content === '' ) { return false; } - let selfie; - try { - selfie = JSON.parse(details.content); - } catch (ex) { - } - if ( typeof selfie !== 'object' || selfie === null ) { return false; } if ( selfie.version !== this.selfieVersion ) { return false; } this.processedFilterCount = selfie.processedFilterCount; this.acceptedCount = selfie.acceptedCount; this.discardedCount = selfie.discardedCount; - this.bitsToBucket = new Map(selfie.bitsToBucket.map(kv => { - kv[1] = new Map(kv[1]); - return kv; - })); + this.bitsToBucket = selfie.bitsToBucket; urlTokenizer.fromSelfie(selfie.urlTokenizer); // If this point is never reached, it means the internal state is @@ -4804,7 +4695,7 @@ FilterContainer.prototype.fromSelfie = async function(storage, path) { return true; }; -FilterContainer.prototype.unserialize = async function(s) { +StaticNetFilteringEngine.prototype.unserialize = async function(s) { const selfie = new Map(JSON.parse(s)); const storage = { async get(name) { @@ -4816,13 +4707,13 @@ FilterContainer.prototype.unserialize = async function(s) { /******************************************************************************/ -FilterContainer.prototype.createCompiler = function() { +StaticNetFilteringEngine.prototype.createCompiler = function() { return new FilterCompiler(); }; /******************************************************************************/ -FilterContainer.prototype.fromCompiled = function(reader) { +StaticNetFilteringEngine.prototype.fromCompiled = function(reader) { reader.select('NETWORK_FILTERS:GOOD'); while ( reader.next() ) { this.acceptedCount += 1; @@ -4841,7 +4732,7 @@ FilterContainer.prototype.fromCompiled = function(reader) { /******************************************************************************/ -FilterContainer.prototype.matchAndFetchModifiers = function( +StaticNetFilteringEngine.prototype.matchAndFetchModifiers = function( fctxt, modifierName ) { @@ -5018,7 +4909,7 @@ FilterContainer.prototype.matchAndFetchModifiers = function( /******************************************************************************/ -FilterContainer.prototype.realmMatchString = function( +StaticNetFilteringEngine.prototype.realmMatchString = function( realmBits, typeBits, partyBits @@ -5145,7 +5036,7 @@ FilterContainer.prototype.realmMatchString = function( // https://www.reddit.com/r/uBlockOrigin/comments/d6vxzj/ // Add support for `specifichide`. -FilterContainer.prototype.matchRequestReverse = function(type, url) { +StaticNetFilteringEngine.prototype.matchRequestReverse = function(type, url) { const typeBits = typeNameToTypeValue[type] | 0x80000000; // Prime tokenizer: we get a normalized URL in return. @@ -5194,7 +5085,7 @@ FilterContainer.prototype.matchRequestReverse = function(type, url) { * * @returns {integer} 0=no match, 1=block, 2=allow (exception) */ -FilterContainer.prototype.matchRequest = function(fctxt, modifiers = 0) { +StaticNetFilteringEngine.prototype.matchRequest = function(fctxt, modifiers = 0) { let typeBits = typeNameToTypeValue[fctxt.type]; if ( modifiers === 0 ) { if ( typeBits === undefined ) { @@ -5241,7 +5132,7 @@ FilterContainer.prototype.matchRequest = function(fctxt, modifiers = 0) { /******************************************************************************/ -FilterContainer.prototype.matchHeaders = function(fctxt, headers) { +StaticNetFilteringEngine.prototype.matchHeaders = function(fctxt, headers) { const typeBits = typeNameToTypeValue[fctxt.type] || otherTypeBitValue; const partyBits = fctxt.is3rdPartyToDoc() ? THIRDPARTY_REALM : FIRSTPARTY_REALM; @@ -5278,7 +5169,7 @@ FilterContainer.prototype.matchHeaders = function(fctxt, headers) { /******************************************************************************/ -FilterContainer.prototype.redirectRequest = function(redirectEngine, fctxt) { +StaticNetFilteringEngine.prototype.redirectRequest = function(redirectEngine, fctxt) { const directives = this.matchAndFetchModifiers(fctxt, 'redirect-rule'); // No directive is the most common occurrence. if ( directives === undefined ) { return; } @@ -5296,28 +5187,40 @@ FilterContainer.prototype.redirectRequest = function(redirectEngine, fctxt) { return directives; }; -FilterContainer.prototype.transformRequest = function(fctxt) { +StaticNetFilteringEngine.prototype.transformRequest = function(fctxt) { const directives = this.matchAndFetchModifiers(fctxt, 'uritransform'); if ( directives === undefined ) { return; } - const directive = directives[directives.length-1]; - if ( (directive.bits & ALLOW_REALM) !== 0 ) { return directives; } - if ( directive.refs instanceof Object === false ) { return; } - const { refs } = directive; - if ( refs.$cache === null ) { - refs.$cache = sfp.parseReplaceValue(refs.value); - } - const cache = refs.$cache; - if ( cache === undefined ) { return; } const redirectURL = new URL(fctxt.url); - const before = redirectURL.pathname + redirectURL.search; - if ( cache.re.test(before) !== true ) { return; } - const after = before.replace(cache.re, cache.replacement); - if ( after === before ) { return; } - const searchPos = after.includes('?') && after.indexOf('?') || after.length; - redirectURL.pathname = after.slice(0, searchPos); - redirectURL.search = after.slice(searchPos); - fctxt.redirectURL = redirectURL.href; - return directives; + const out = []; + for ( const directive of directives ) { + if ( (directive.bits & ALLOW_REALM) !== 0 ) { + out.push(directive); + continue; + } + const { refs } = directive; + if ( refs instanceof Object === false ) { continue; } + if ( refs.$cache === null ) { + refs.$cache = sfp.parseReplaceValue(refs.value); + } + const cache = refs.$cache; + if ( cache === undefined ) { continue; } + const before = `${redirectURL.pathname}${redirectURL.search}${redirectURL.hash}`; + if ( cache.re.test(before) !== true ) { continue; } + const after = before.replace(cache.re, cache.replacement); + if ( after === before ) { continue; } + const hashPos = after.indexOf('#'); + redirectURL.hash = hashPos !== -1 ? after.slice(hashPos) : ''; + const afterMinusHash = hashPos !== -1 ? after.slice(0, hashPos) : after; + const searchPos = afterMinusHash.indexOf('?'); + redirectURL.search = searchPos !== -1 ? afterMinusHash.slice(searchPos) : ''; + redirectURL.pathname = searchPos !== -1 ? after.slice(0, searchPos) : after; + out.push(directive); + } + if ( out.length === 0 ) { return; } + if ( redirectURL.href !== fctxt.url ) { + fctxt.redirectURL = redirectURL.href; + } + return out; }; function parseRedirectRequestValue(directive) { @@ -5348,7 +5251,7 @@ function compareRedirectRequests(redirectEngine, a, b) { // https://github.com/uBlockOrigin/uBlock-issues/issues/1626 // Do not redirect when the number of query parameters does not change. -FilterContainer.prototype.filterQuery = function(fctxt) { +StaticNetFilteringEngine.prototype.filterQuery = function(fctxt) { const directives = this.matchAndFetchModifiers(fctxt, 'removeparam'); if ( directives === undefined ) { return; } const url = fctxt.url; @@ -5422,7 +5325,7 @@ FilterContainer.prototype.filterQuery = function(fctxt) { fctxt.redirectURL = url.slice(0, qpos); if ( params.size !== 0 ) { fctxt.redirectURL += '?' + Array.from(params).map(a => - a[1] === '' ? a[0] : `${a[0]}=${a[1]}` + a[1] === '' ? `${a[0]}=` : `${a[0]}=${a[1]}` ).join('&'); } if ( hpos !== url.length ) { @@ -5442,14 +5345,14 @@ function parseQueryPruneValue(directive) { /******************************************************************************/ -FilterContainer.prototype.hasQuery = function(fctxt) { +StaticNetFilteringEngine.prototype.hasQuery = function(fctxt) { urlTokenizer.setURL(fctxt.url); return urlTokenizer.hasQuery(); }; /******************************************************************************/ -FilterContainer.prototype.toLogData = function() { +StaticNetFilteringEngine.prototype.toLogData = function() { if ( this.$filterUnit !== 0 ) { return new LogData(this.$catBits, this.$tokenHash, this.$filterUnit); } @@ -5457,19 +5360,19 @@ FilterContainer.prototype.toLogData = function() { /******************************************************************************/ -FilterContainer.prototype.isBlockImportant = function() { +StaticNetFilteringEngine.prototype.isBlockImportant = function() { return this.$filterUnit !== 0 && $isBlockImportant; }; /******************************************************************************/ -FilterContainer.prototype.getFilterCount = function() { +StaticNetFilteringEngine.prototype.getFilterCount = function() { return this.acceptedCount - this.discardedCount; }; /******************************************************************************/ -FilterContainer.prototype.enableWASM = function(wasmModuleFetcher, path) { +StaticNetFilteringEngine.prototype.enableWASM = function(wasmModuleFetcher, path) { return Promise.all([ bidiTrie.enableWASM(wasmModuleFetcher, path), origHNTrieContainer.enableWASM(wasmModuleFetcher, path), @@ -5481,7 +5384,7 @@ FilterContainer.prototype.enableWASM = function(wasmModuleFetcher, path) { /******************************************************************************/ -FilterContainer.prototype.test = async function(docURL, type, url) { +StaticNetFilteringEngine.prototype.test = async function(docURL, type, url) { const fctxt = new FilteringContext(); fctxt.setDocOriginFromURL(docURL); fctxt.setType(type); @@ -5495,7 +5398,7 @@ FilterContainer.prototype.test = async function(docURL, type, url) { /******************************************************************************/ -FilterContainer.prototype.bucketHistogram = function() { +StaticNetFilteringEngine.prototype.bucketHistogram = function() { const results = []; for ( const [ bits, bucket ] of this.bitsToBucket ) { for ( const [ th, iunit ] of bucket ) { @@ -5516,7 +5419,7 @@ FilterContainer.prototype.bucketHistogram = function() { // Dump the internal state of the filtering engine to the console. // Useful to make development decisions and investigate issues. -FilterContainer.prototype.dump = function() { +StaticNetFilteringEngine.prototype.dump = function() { const thConstants = new Map([ [ NO_TOKEN_HASH, 'NO_TOKEN_HASH' ], [ DOT_TOKEN_HASH, 'DOT_TOKEN_HASH' ], @@ -5646,6 +5549,6 @@ FilterContainer.prototype.dump = function() { /******************************************************************************/ -const staticNetFilteringEngine = new FilterContainer(); +const staticNetFilteringEngine = new StaticNetFilteringEngine(); export default staticNetFilteringEngine; diff --git a/src/js/storage.js b/src/js/storage.js index 151717c..cd340fc 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -19,44 +19,39 @@ Home: https://github.com/gorhill/uBlock */ -'use strict'; - /******************************************************************************/ -import publicSuffixList from '../lib/publicsuffixlist/publicsuffixlist.js'; -import punycode from '../lib/punycode.js'; +import * as sfp from './static-filtering-parser.js'; -import io from './assets.js'; +import { CompiledListReader, CompiledListWriter } from './static-filtering-io.js'; +import { LineIterator, orphanizeString } from './text-utils.js'; import { broadcast, filteringBehaviorChanged, onBroadcast } from './broadcast.js'; +import { i18n, i18n$ } from './i18n.js'; +import { + permanentFirewall, + permanentSwitches, + permanentURLFiltering, +} from './filtering-engines.js'; +import { ubolog, ubologSet } from './console.js'; + import cosmeticFilteringEngine from './cosmetic-filtering.js'; +import { hostnameFromURI } from './uri-utils.js'; +import io from './assets.js'; import logger from './logger.js'; import lz4Codec from './lz4.js'; +import publicSuffixList from '../lib/publicsuffixlist/publicsuffixlist.js'; +import punycode from '../lib/punycode.js'; +import { redirectEngine } from './redirect-engine.js'; import staticExtFilteringEngine from './static-ext-filtering.js'; import staticFilteringReverseLookup from './reverselookup.js'; import staticNetFilteringEngine from './static-net-filtering.js'; import µb from './background.js'; -import { hostnameFromURI } from './uri-utils.js'; -import { i18n, i18n$ } from './i18n.js'; -import { redirectEngine } from './redirect-engine.js'; -import { sparseBase64 } from './base64-custom.js'; -import { ubolog, ubologSet } from './console.js'; -import * as sfp from './static-filtering-parser.js'; -import { - permanentFirewall, - permanentSwitches, - permanentURLFiltering, -} from './filtering-engines.js'; - -import { - CompiledListReader, - CompiledListWriter, -} from './static-filtering-io.js'; +/******************************************************************************/ -import { - LineIterator, - orphanizeString, -} from './text-utils.js'; +// https://eslint.org/docs/latest/rules/no-prototype-builtins +const hasOwnProperty = (o, p) => + Object.prototype.hasOwnProperty.call(o, p); /******************************************************************************/ @@ -98,24 +93,80 @@ import { /******************************************************************************/ { - let localSettingsLastSaved = Date.now(); + const requestStats = µb.requestStats; + let requestStatsDisabled = false; + + µb.loadLocalSettings = async ( ) => { + requestStatsDisabled = µb.hiddenSettings.requestStatsDisabled; + if ( requestStatsDisabled ) { return; } + return Promise.all([ + vAPI.sessionStorage.get('requestStats'), + vAPI.storage.get('requestStats'), + vAPI.storage.get([ 'blockedRequestCount', 'allowedRequestCount' ]), + ]).then(([ a, b, c ]) => { + if ( a instanceof Object && a.requestStats ) { return a.requestStats; } + if ( b instanceof Object && b.requestStats ) { return b.requestStats; } + if ( c instanceof Object && Object.keys(c).length === 2 ) { + return { + blockedCount: c.blockedRequestCount, + allowedCount: c.allowedRequestCount, + }; + } + return { blockedCount: 0, allowedCount: 0 }; + }).then(({ blockedCount, allowedCount }) => { + requestStats.blockedCount += blockedCount; + requestStats.allowedCount += allowedCount; + }); + }; - const shouldSave = ( ) => { - if ( µb.localSettingsLastModified > localSettingsLastSaved ) { - µb.saveLocalSettings(); - } - saveTimer.on(saveDelay); + const SAVE_DELAY_IN_MINUTES = 3.6; + const QUICK_SAVE_DELAY_IN_SECONDS = 23; + + const stopTimers = ( ) => { + vAPI.alarms.clear('saveLocalSettings'); + quickSaveTimer.off(); + saveTimer.off(); }; - const saveTimer = vAPI.defer.create(shouldSave); - const saveDelay = { sec: 23 }; + const saveTimer = vAPI.defer.create(( ) => { + µb.saveLocalSettings(); + }); + + const quickSaveTimer = vAPI.defer.create(( ) => { + if ( vAPI.sessionStorage.unavailable !== true ) { + vAPI.sessionStorage.set({ requestStats: requestStats }); + } + if ( requestStatsDisabled ) { return; } + saveTimer.on({ min: SAVE_DELAY_IN_MINUTES }); + vAPI.alarms.createIfNotPresent('saveLocalSettings', { + delayInMinutes: SAVE_DELAY_IN_MINUTES + 0.5 + }); + }); - saveTimer.onidle(saveDelay); + µb.incrementRequestStats = (blocked, allowed) => { + requestStats.blockedCount += blocked; + requestStats.allowedCount += allowed; + quickSaveTimer.on({ sec: QUICK_SAVE_DELAY_IN_SECONDS }); + }; - µb.saveLocalSettings = function() { - localSettingsLastSaved = Date.now(); - return vAPI.storage.set(this.localSettings); + µb.saveLocalSettings = ( ) => { + stopTimers(); + if ( requestStatsDisabled ) { return; } + return vAPI.storage.set({ requestStats: µb.requestStats }); }; + + onBroadcast(msg => { + if ( msg.what !== 'hiddenSettingsChanged' ) { return; } + const newState = µb.hiddenSettings.requestStatsDisabled; + if ( requestStatsDisabled === newState ) { return; } + requestStatsDisabled = newState; + if ( newState ) { + stopTimers(); + µb.requestStats.blockedCount = µb.requestStats.allowedCount = 0; + } else { + µb.loadLocalSettings(); + } + }); } /******************************************************************************/ @@ -136,7 +187,7 @@ import { for ( const entry of adminSettings ) { if ( entry.length < 1 ) { continue; } const name = entry[0]; - if ( usDefault.hasOwnProperty(name) === false ) { continue; } + if ( hasOwnProperty(usDefault, name) === false ) { continue; } const value = entry.length < 2 ? usDefault[name] : this.settingValueFromString(usDefault, name, entry[1]); @@ -165,8 +216,8 @@ import { const toRemove = []; for ( const key in this.userSettings ) { - if ( this.userSettings.hasOwnProperty(key) === false ) { continue; } - if ( toSave.hasOwnProperty(key) ) { continue; } + if ( hasOwnProperty(this.userSettings, key) === false ) { continue; } + if ( hasOwnProperty(toSave, key) ) { continue; } toRemove.push(key); } if ( toRemove.length !== 0 ) { @@ -203,7 +254,7 @@ import { for ( const entry of advancedSettings ) { if ( entry.length < 1 ) { continue; } const name = entry[0]; - if ( hsDefault.hasOwnProperty(name) === false ) { continue; } + if ( hasOwnProperty(hsDefault, name) === false ) { continue; } const value = entry.length < 2 ? hsDefault[name] : this.hiddenSettingValueFromString(name, entry[1]); @@ -237,8 +288,8 @@ import { } for ( const key in hsDefault ) { - if ( hsDefault.hasOwnProperty(key) === false ) { continue; } - if ( hsAdmin.hasOwnProperty(name) ) { continue; } + if ( hasOwnProperty(hsDefault, key) === false ) { continue; } + if ( hasOwnProperty(hsAdmin, name) ) { continue; } if ( typeof hs[key] !== typeof hsDefault[key] ) { continue; } this.hiddenSettings[key] = hs[key]; } @@ -283,8 +334,8 @@ onBroadcast(msg => { const matches = /^\s*(\S+)\s+(.+)$/.exec(line); if ( matches === null || matches.length !== 3 ) { continue; } const name = matches[1]; - if ( out.hasOwnProperty(name) === false ) { continue; } - if ( this.hiddenSettingsAdmin.hasOwnProperty(name) ) { continue; } + if ( hasOwnProperty(out, name) === false ) { continue; } + if ( hasOwnProperty(this.hiddenSettingsAdmin, name) ) { continue; } const value = this.hiddenSettingValueFromString(name, matches[2]); if ( value !== undefined ) { out[name] = value; @@ -296,7 +347,7 @@ onBroadcast(msg => { µb.hiddenSettingValueFromString = function(name, value) { if ( typeof name !== 'string' || typeof value !== 'string' ) { return; } const hsDefault = this.hiddenSettingsDefault; - if ( hsDefault.hasOwnProperty(name) === false ) { return; } + if ( hasOwnProperty(hsDefault, name) === false ) { return; } let r; switch ( typeof hsDefault[name] ) { case 'boolean': @@ -369,6 +420,9 @@ onBroadcast(msg => { /******************************************************************************/ µb.isTrustedList = function(assetKey) { + if ( assetKey === this.userFiltersPath ) { + if ( this.userSettings.userFiltersTrusted ) { return true; } + } if ( this.parsedTrustedListPrefixes.length === 0 ) { this.parsedTrustedListPrefixes = µb.hiddenSettings.trustedListPrefixes.split(/ +/).map(prefix => { @@ -530,7 +584,6 @@ onBroadcast(msg => { // https://github.com/gorhill/uBlock/issues/1022 // Be sure to end with an empty line. content = content.trim(); - if ( content !== '' ) { content += '\n'; } this.removeCompiledFilterList(this.userFiltersPath); return io.put(this.userFiltersPath, content); }; @@ -626,6 +679,11 @@ onBroadcast(msg => { cosmeticFilteringEngine.removeFromSelectorCache( hostnameFromURI(details.docURL) ); + staticFilteringReverseLookup.resetLists(); +}; + +µb.userFiltersAreEnabled = function() { + return this.selectedFilterLists.includes(this.userFiltersPath); }; /******************************************************************************/ @@ -633,7 +691,7 @@ onBroadcast(msg => { µb.autoSelectRegionalFilterLists = function(lists) { const selectedListKeys = [ this.userFiltersPath ]; for ( const key in lists ) { - if ( lists.hasOwnProperty(key) === false ) { continue; } + if ( hasOwnProperty(lists, key) === false ) { continue; } const list = lists[key]; if ( list.content !== 'filters' ) { continue; } if ( list.off !== true ) { @@ -845,8 +903,10 @@ onBroadcast(msg => { let loadingPromise; let t0 = 0; + const elapsed = ( ) => `${Date.now() - t0} ms`; + const onDone = ( ) => { - ubolog(`loadFilterLists() took ${Date.now()-t0} ms`); + ubolog(`loadFilterLists() All filters in memory at ${elapsed()}`); staticNetFilteringEngine.freeze(); staticExtFilteringEngine.freeze(); @@ -854,14 +914,16 @@ onBroadcast(msg => { vAPI.net.unsuspend(); filteringBehaviorChanged(); - vAPI.storage.set({ 'availableFilterLists': µb.availableFilterLists }); + ubolog(`loadFilterLists() All filters ready at ${elapsed()}`); logger.writeOne({ realm: 'message', type: 'info', - text: 'Reloading all filter lists: done' + text: `Reloading all filter lists: done, took ${elapsed()}` }); + vAPI.storage.set({ 'availableFilterLists': µb.availableFilterLists }); + broadcast({ what: 'staticFilteringDataChanged', parseCosmeticFilters: µb.userSettings.parseAllABPHideFilters, @@ -877,12 +939,13 @@ onBroadcast(msg => { }; const applyCompiledFilters = (assetKey, compiled) => { + ubolog(`loadFilterLists() Loading filters from ${assetKey} at ${elapsed()}`); const snfe = staticNetFilteringEngine; const sxfe = staticExtFilteringEngine; let acceptedCount = snfe.acceptedCount + sxfe.acceptedCount; let discardedCount = snfe.discardedCount + sxfe.discardedCount; µb.applyCompiledFilters(compiled, assetKey === µb.userFiltersPath); - if ( µb.availableFilterLists.hasOwnProperty(assetKey) ) { + if ( hasOwnProperty(µb.availableFilterLists, assetKey) ) { const entry = µb.availableFilterLists[assetKey]; entry.entryCount = snfe.acceptedCount + sxfe.acceptedCount - acceptedCount; @@ -910,13 +973,15 @@ onBroadcast(msg => { µb.selfieManager.destroy(); staticFilteringReverseLookup.resetLists(); + ubolog(`loadFilterLists() All filters removed at ${elapsed()}`); + // We need to build a complete list of assets to pull first: this is // because it *may* happens that some load operations are synchronous: // This happens for assets which do not exist, or assets with no // content. const toLoad = []; for ( const assetKey in lists ) { - if ( lists.hasOwnProperty(assetKey) === false ) { continue; } + if ( hasOwnProperty(lists, assetKey) === false ) { continue; } if ( lists[assetKey].off ) { continue; } toLoad.push( µb.getCompiledFilterList(assetKey).then(details => { @@ -945,11 +1010,14 @@ onBroadcast(msg => { µb.loadFilterLists = function() { if ( loadingPromise instanceof Promise ) { return loadingPromise; } + ubolog('loadFilterLists() Start'); t0 = Date.now(); loadedListKeys.length = 0; loadingPromise = Promise.all([ this.getAvailableLists().then(lists => onFilterListsReady(lists)), - this.loadRedirectResources(), + this.loadRedirectResources().then(( ) => { + ubolog(`loadFilterLists() Redirects/scriptlets ready at ${elapsed()}`); + }), ]).then(( ) => { onDone(); }); @@ -960,7 +1028,7 @@ onBroadcast(msg => { /******************************************************************************/ µb.getCompiledFilterList = async function(assetKey) { - const compiledPath = 'compiled/' + assetKey; + const compiledPath = `compiled/${assetKey}`; // https://github.com/uBlockOrigin/uBlock-issues/issues/1365 // Verify that the list version matches that of the current compiled @@ -969,11 +1037,10 @@ onBroadcast(msg => { this.compiledFormatChanged === false && this.badLists.has(assetKey) === false ) { - const compiledDetails = await io.get(compiledPath); + const content = await io.fromCache(compiledPath); const compilerVersion = `${this.systemSettings.compiledMagic}\n`; - if ( compiledDetails.content.startsWith(compilerVersion) ) { - compiledDetails.assetKey = assetKey; - return compiledDetails; + if ( content.startsWith(compilerVersion) ) { + return { assetKey, content }; } } @@ -1003,7 +1070,7 @@ onBroadcast(msg => { assetKey, trustedSource: this.isTrustedList(assetKey), }); - io.put(compiledPath, compiledContent); + io.toCache(compiledPath, compiledContent); return { assetKey, content: compiledContent }; }; @@ -1032,7 +1099,7 @@ onBroadcast(msg => { /******************************************************************************/ µb.removeCompiledFilterList = function(assetKey) { - io.remove('compiled/' + assetKey); + io.remove(`compiled/${assetKey}`); }; µb.removeFilterList = function(assetKey) { @@ -1135,7 +1202,10 @@ onBroadcast(msg => { µb.loadRedirectResources = async function() { try { const success = await redirectEngine.resourcesFromSelfie(io); - if ( success === true ) { return true; } + if ( success === true ) { + ubolog('Loaded redirect/scriptlets resources from selfie'); + return true; + } const fetcher = (path, options = undefined) => { if ( path.startsWith('/web_accessible_resources/') ) { @@ -1159,20 +1229,17 @@ onBroadcast(msg => { const results = await Promise.all(fetchPromises); if ( Array.isArray(results) === false ) { return results; } - let content = ''; + const content = []; for ( let i = 1; i < results.length; i++ ) { const result = results[i]; - if ( - result instanceof Object === false || - typeof result.content !== 'string' || - result.content === '' - ) { - continue; - } - content += '\n\n' + result.content; + if ( result instanceof Object === false ) { continue; } + if ( typeof result.content !== 'string' ) { continue; } + if ( result.content === '' ) { continue; } + content.push(result.content); + } + if ( content.length !== 0 ) { + redirectEngine.resourcesFromString(content.join('\n\n')); } - - redirectEngine.resourcesFromString(content); redirectEngine.selfieFromResources(io); } catch(ex) { ubolog(ex); @@ -1211,8 +1278,11 @@ onBroadcast(msg => { } try { - const result = await io.get(`compiled/${this.pslAssetKey}`); - if ( psl.fromSelfie(result.content, sparseBase64) ) { return; } + const selfie = await io.fromCache(`selfie/${this.pslAssetKey}`); + if ( psl.fromSelfie(selfie) ) { + ubolog('Loaded PSL from selfie'); + return; + } } catch (reason) { ubolog(reason); } @@ -1226,7 +1296,8 @@ onBroadcast(msg => { µb.compilePublicSuffixList = function(content) { const psl = publicSuffixList; psl.parse(content, punycode.toASCII); - io.put(`compiled/${this.pslAssetKey}`, psl.toSelfie(sparseBase64)); + ubolog(`Loaded PSL from ${this.pslAssetKey}`); + return io.toCache(`selfie/${this.pslAssetKey}`, psl.toSelfie()); }; /******************************************************************************/ @@ -1246,39 +1317,24 @@ onBroadcast(msg => { if ( µb.inMemoryFilters.length !== 0 ) { return; } if ( Object.keys(µb.availableFilterLists).length === 0 ) { return; } await Promise.all([ - io.put( - 'selfie/main', - JSON.stringify({ - magic: µb.systemSettings.selfieMagic, - availableFilterLists: µb.availableFilterLists, - }) - ), - redirectEngine.toSelfie('selfie/redirectEngine'), - staticExtFilteringEngine.toSelfie( - 'selfie/staticExtFilteringEngine' + io.toCache('selfie/staticMain', { + magic: µb.systemSettings.selfieMagic, + availableFilterLists: µb.availableFilterLists, + }), + io.toCache('selfie/staticExtFilteringEngine', + staticExtFilteringEngine.toSelfie() ), - staticNetFilteringEngine.toSelfie(io, - 'selfie/staticNetFilteringEngine' + io.toCache('selfie/staticNetFilteringEngine', + staticNetFilteringEngine.toSelfie() ), ]); lz4Codec.relinquish(); µb.selfieIsInvalid = false; + ubolog('Filtering engine selfie created'); }; const loadMain = async function() { - const details = await io.get('selfie/main'); - if ( - details instanceof Object === false || - typeof details.content !== 'string' || - details.content === '' - ) { - return false; - } - let selfie; - try { - selfie = JSON.parse(details.content); - } catch(ex) { - } + const selfie = await io.fromCache('selfie/staticMain'); if ( selfie instanceof Object === false ) { return false; } if ( selfie.magic !== µb.systemSettings.selfieMagic ) { return false; } if ( selfie.availableFilterLists instanceof Object === false ) { return false; } @@ -1292,12 +1348,11 @@ onBroadcast(msg => { try { const results = await Promise.all([ loadMain(), - redirectEngine.fromSelfie('selfie/redirectEngine'), - staticExtFilteringEngine.fromSelfie( - 'selfie/staticExtFilteringEngine' + io.fromCache('selfie/staticExtFilteringEngine').then(selfie => + staticExtFilteringEngine.fromSelfie(selfie) ), - staticNetFilteringEngine.fromSelfie(io, - 'selfie/staticNetFilteringEngine' + io.fromCache('selfie/staticNetFilteringEngine').then(selfie => + staticNetFilteringEngine.fromSelfie(selfie) ), ]); if ( results.every(v => v) ) { @@ -1307,33 +1362,26 @@ onBroadcast(msg => { catch (reason) { ubolog(reason); } + ubolog('Filtering engine selfie not available'); destroy(); return false; }; - const destroy = function() { + const destroy = function(options = {}) { if ( µb.selfieIsInvalid === false ) { - io.remove(/^selfie\//); + io.remove(/^selfie\/static/, options); µb.selfieIsInvalid = true; - } - if ( µb.wakeupReason === 'createSelfie' ) { - µb.wakeupReason = ''; - return createTimer.offon({ sec: 27 }); + ubolog('Filtering engine selfie marked for invalidation'); } vAPI.alarms.create('createSelfie', { - delayInMinutes: µb.hiddenSettings.selfieAfter + delayInMinutes: (µb.hiddenSettings.selfieDelayInSeconds + 17) / 60, }); - createTimer.offon({ min: µb.hiddenSettings.selfieAfter }); + createTimer.offon({ sec: µb.hiddenSettings.selfieDelayInSeconds }); }; const createTimer = vAPI.defer.create(create); - vAPI.alarms.onAlarm.addListener(alarm => { - if ( alarm.name !== 'createSelfie') { return; } - µb.wakeupReason = 'createSelfie'; - }); - - µb.selfieManager = { load, destroy }; + µb.selfieManager = { load, create, destroy }; } /******************************************************************************/ @@ -1385,8 +1433,8 @@ onBroadcast(msg => { const µbus = this.userSettings; const adminus = data.userSettings; for ( const name in µbus ) { - if ( µbus.hasOwnProperty(name) === false ) { continue; } - if ( adminus.hasOwnProperty(name) === false ) { continue; } + if ( hasOwnProperty(µbus, name) === false ) { continue; } + if ( hasOwnProperty(adminus, name) === false ) { continue; } bin[name] = adminus[name]; binNotEmpty = true; } @@ -1449,13 +1497,21 @@ onBroadcast(msg => { vAPI.storage.set(bin); } - if ( - Array.isArray(toOverwrite.filters) && - toOverwrite.filters.length !== 0 - ) { - this.saveUserFilters(toOverwrite.filters.join('\n')); + let userFiltersAfter; + if ( Array.isArray(toOverwrite.filters) ) { + userFiltersAfter = toOverwrite.filters.join('\n').trim(); } else if ( typeof data.userFilters === 'string' ) { - this.saveUserFilters(data.userFilters); + userFiltersAfter = data.userFilters.trim(); + } + if ( typeof userFiltersAfter === 'string' ) { + const bin = await vAPI.storage.get(this.userFiltersPath); + const userFiltersBefore = bin && bin[this.userFiltersPath] || ''; + if ( userFiltersAfter !== userFiltersBefore ) { + await Promise.all([ + this.saveUserFilters(userFiltersAfter), + this.selfieManager.destroy(), + ]); + } } }; @@ -1493,7 +1549,6 @@ onBroadcast(msg => { { let next = 0; - let lastEmergencyUpdate = 0; const launchTimer = vAPI.defer.create(fetchDelay => { next = 0; @@ -1502,6 +1557,7 @@ onBroadcast(msg => { µb.scheduleAssetUpdater = async function(details = {}) { launchTimer.off(); + vAPI.alarms.clear('assetUpdater'); if ( details.now ) { next = 0; @@ -1520,40 +1576,23 @@ onBroadcast(msg => { this.hiddenSettings.autoUpdatePeriod * 3600000; const now = Date.now(); - let needEmergencyUpdate = false; - - // Respect cooldown period before launching an emergency update. - const timeSinceLastEmergencyUpdate = (now - lastEmergencyUpdate) / 3600000; - if ( timeSinceLastEmergencyUpdate > 1 ) { - const entries = await io.getUpdateAges({ - filters: µb.selectedFilterLists, - internal: [ '*' ], - }); - for ( const entry of entries ) { - if ( entry.ageNormalized < 2 ) { continue; } - needEmergencyUpdate = true; - lastEmergencyUpdate = now; - break; - } - } // Use the new schedule if and only if it is earlier than the previous // one. if ( next !== 0 ) { - updateDelay = Math.min(updateDelay, Math.max(next - now, 0)); - } - - if ( needEmergencyUpdate ) { - updateDelay = Math.min(updateDelay, 15000); + updateDelay = Math.min(updateDelay, Math.max(next - now, 1)); } next = now + updateDelay; - const fetchDelay = needEmergencyUpdate - ? 2000 - : this.hiddenSettings.autoUpdateAssetFetchPeriod * 1000 || 60000; + const fetchDelay = details.fetchDelay || + this.hiddenSettings.autoUpdateAssetFetchPeriod * 1000 || + 60000; launchTimer.on(updateDelay, fetchDelay); + vAPI.alarms.create('assetUpdater', { + delayInMinutes: Math.ceil(updateDelay / 60000) + 0.25 + }); }; } @@ -1566,7 +1605,7 @@ onBroadcast(msg => { if ( topic === 'before-asset-updated' ) { if ( details.type === 'filters' ) { if ( - this.availableFilterLists.hasOwnProperty(details.assetKey) === false || + hasOwnProperty(this.availableFilterLists, details.assetKey) === false || this.selectedFilterLists.indexOf(details.assetKey) === -1 || this.badLists.get(details.assetKey) ) { @@ -1580,9 +1619,8 @@ onBroadcast(msg => { if ( topic === 'after-asset-updated' ) { // Skip selfie-related content. if ( details.assetKey.startsWith('selfie/') ) { return; } - const cached = typeof details.content === 'string' && - details.content !== ''; - if ( this.availableFilterLists.hasOwnProperty(details.assetKey) ) { + const cached = typeof details.content === 'string' && details.content !== ''; + if ( hasOwnProperty(this.availableFilterLists, details.assetKey) ) { if ( cached ) { if ( this.selectedFilterLists.indexOf(details.assetKey) !== -1 ) { this.extractFilterListMetadata( @@ -1590,8 +1628,7 @@ onBroadcast(msg => { details.content ); if ( this.badLists.has(details.assetKey) === false ) { - io.put( - 'compiled/' + details.assetKey, + io.toCache(`compiled/${details.assetKey}`, this.compileFilters(details.content, { assetKey: details.assetKey, trustedSource: this.isTrustedList(details.assetKey), diff --git a/src/js/traffic.js b/src/js/traffic.js index bf34fd4..df86a86 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -551,7 +551,7 @@ const onHeadersReceived = function(details) { } } if ( jobs.length !== 0 ) { - bodyFilterer.doFilter(fctxt, jobs); + bodyFilterer.doFilter(details.requestId, fctxt, jobs); } } @@ -590,7 +590,7 @@ const onHeadersReceived = function(details) { } }; -const reMediaContentTypes = /^(?:audio|image|video)\//; +const reMediaContentTypes = /^(?:audio|image|video)\/|(?:\/ogg)$/; /******************************************************************************/ @@ -749,7 +749,7 @@ const bodyFilterer = (( ) => { /* t */ if ( bytes[i+6] !== 0x74 ) { continue; } break; } - if ( (i - 40) >= 65536 ) { return; } + if ( (i + 40) >= 65536 ) { return; } i += 8; // find first alpha character let j = -1; @@ -827,13 +827,17 @@ const bodyFilterer = (( ) => { } if ( this.status !== 'finishedtransferringdata' ) { return; } - // If encoding is still unknown, try to extract from stream data + // If encoding is still unknown, try to extract from stream data. + // Just assume utf-8 if ultimately no encoding can be looked up. if ( session.charset === undefined ) { const charsetFound = charsetFromStream(session.buffer); - if ( charsetFound === undefined ) { return streamClose(session); } - const charsetUsed = textEncode.normalizeCharset(charsetFound); - if ( charsetUsed === undefined ) { return streamClose(session); } - session.charset = charsetUsed; + if ( charsetFound !== undefined ) { + const charsetUsed = textEncode.normalizeCharset(charsetFound); + if ( charsetUsed === undefined ) { return streamClose(session); } + session.charset = charsetUsed; + } else { + session.charset = 'utf-8'; + } } while ( session.jobs.length !== 0 ) { @@ -886,10 +890,10 @@ const bodyFilterer = (( ) => { this.str = s; this.modified = true; } - static doFilter(fctxt, jobs) { + static doFilter(requestId, fctxt, jobs) { if ( jobs.length === 0 ) { return; } const session = new Session(fctxt, mime, charset, jobs); - session.stream = browser.webRequest.filterResponseData(session.id); + session.stream = browser.webRequest.filterResponseData(requestId); session.stream.ondata = onStreamData; session.stream.onstop = onStreamStop; session.stream.onerror = onStreamError; diff --git a/src/js/ublock.js b/src/js/ublock.js index e963377..cfc6349 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -148,7 +148,7 @@ const matchBucket = function(url, hostname, bucket, start) { } bucket.push(directive); this.saveWhitelist(); - filteringBehaviorChanged({ hostname: targetHostname }); + filteringBehaviorChanged({ hostname: targetHostname, direction: -1 }); return true; } diff --git a/src/js/whitelist.js b/src/js/whitelist.js index e7905ee..b8f0eaa 100644 --- a/src/js/whitelist.js +++ b/src/js/whitelist.js @@ -30,12 +30,12 @@ import { dom, qs$ } from './dom.js'; const reComment = /^\s*#\s*/; -const directiveFromLine = function(line) { +function directiveFromLine(line) { const match = reComment.exec(line); return match === null ? line.trim() : line.slice(match.index + match[0].length).trim(); -}; +} /******************************************************************************/ @@ -43,7 +43,7 @@ CodeMirror.defineMode("ubo-whitelist-directives", function() { const reRegex = /^\/.+\/$/; return { - token: function(stream) { + token: function token(stream) { const line = stream.string.trim(); stream.skipToEnd(); if ( reBadHostname === undefined ) { @@ -100,18 +100,18 @@ uBlockDashboard.patchCodeMirrorEditor(cmEditor); /******************************************************************************/ -const getEditorText = function() { +function getEditorText() { let text = cmEditor.getValue().replace(/\s+$/, ''); return text === '' ? text : text + '\n'; -}; +} -const setEditorText = function(text) { +function setEditorText(text) { cmEditor.setValue(text.replace(/\s+$/, '') + '\n'); -}; +} /******************************************************************************/ -const whitelistChanged = function() { +function whitelistChanged() { const whitelistElem = qs$('#whitelist'); const bad = qs$(whitelistElem, '.cm-error') !== null; const changedWhitelist = getEditorText().trim(); @@ -119,13 +119,13 @@ const whitelistChanged = function() { qs$('#whitelistApply').disabled = !changed || bad; qs$('#whitelistRevert').disabled = !changed; CodeMirror.commands.save = changed && !bad ? applyChanges : noopFunc; -}; +} cmEditor.on('changes', whitelistChanged); /******************************************************************************/ -const renderWhitelist = async function() { +async function renderWhitelist() { const details = await messaging.send('dashboard', { what: 'getWhitelist', }); @@ -161,11 +161,11 @@ const renderWhitelist = async function() { if ( first ) { cmEditor.clearHistory(); } -}; +} /******************************************************************************/ -const handleImportFilePicker = function() { +function handleImportFilePicker() { const file = this.files[0]; if ( file === undefined || file.name === '' ) { return; } if ( file.type.indexOf('text') !== 0 ) { return; } @@ -179,22 +179,22 @@ const handleImportFilePicker = function() { setEditorText(content); }; fr.readAsText(file); -}; +} /******************************************************************************/ -const startImportFilePicker = function() { +function startImportFilePicker() { const input = qs$('#importFilePicker'); // Reset to empty string, this will ensure an change event is properly // triggered if the user pick a file, even if it is the same as the last // one picked. input.value = ''; input.click(); -}; +} /******************************************************************************/ -const exportWhitelistToFile = function() { +function exportWhitelistToFile() { const val = getEditorText(); if ( val === '' ) { return; } const filename = @@ -205,42 +205,44 @@ const exportWhitelistToFile = function() { 'url': `data:text/plain;charset=utf-8,${encodeURIComponent(val + '\n')}`, 'filename': filename }); -}; +} /******************************************************************************/ -const applyChanges = async function() { +async function applyChanges() { cachedWhitelist = getEditorText().trim(); await messaging.send('dashboard', { what: 'setWhitelist', whitelist: cachedWhitelist, }); renderWhitelist(); -}; +} -const revertChanges = function() { +function revertChanges() { setEditorText(cachedWhitelist); -}; +} /******************************************************************************/ -const getCloudData = function() { +function getCloudData() { return getEditorText(); -}; +} -const setCloudData = function(data, append) { +function setCloudData(data, append) { if ( typeof data !== 'string' ) { return; } if ( append ) { data = uBlockDashboard.mergeNewLines(getEditorText().trim(), data); } setEditorText(data.trim()); -}; +} self.cloud.onPush = getCloudData; self.cloud.onPull = setCloudData; /******************************************************************************/ +self.wikilink = 'https://github.com/gorhill/uBlock/wiki/Dashboard:-Trusted-sites'; + self.hasUnsavedData = function() { return getEditorText().trim() !== cachedWhitelist; }; |