summaryrefslogtreecommitdiffstats
path: root/src/js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/js/1p-filters.js95
-rw-r--r--src/js/3p-filters.js102
-rw-r--r--src/js/asset-viewer.js1
-rw-r--r--src/js/assets.js183
-rw-r--r--src/js/background.js51
-rw-r--r--src/js/base64-custom.js103
-rw-r--r--src/js/benchmarks.js58
-rw-r--r--src/js/biditrie.js31
-rw-r--r--src/js/broadcast.js18
-rw-r--r--src/js/cachestorage.js983
-rw-r--r--src/js/click2load.js5
-rw-r--r--src/js/codemirror/search.js86
-rw-r--r--src/js/codemirror/ubo-static-filtering.js447
-rw-r--r--src/js/commands.js10
-rw-r--r--src/js/contentscript-extra.js56
-rw-r--r--src/js/contentscript.js31
-rw-r--r--src/js/contextmenu.js6
-rw-r--r--src/js/cosmetic-filtering.js68
-rw-r--r--src/js/dashboard.js18
-rw-r--r--src/js/devtools.js22
-rw-r--r--src/js/dom.js4
-rw-r--r--src/js/dyna-rules.js168
-rw-r--r--src/js/epicker-ui.js57
-rw-r--r--src/js/fa-icons.js2
-rw-r--r--src/js/filtering-context.js2
-rw-r--r--src/js/hntrie.js35
-rw-r--r--src/js/i18n.js36
-rw-r--r--src/js/logger-ui.js124
-rw-r--r--src/js/logger.js32
-rw-r--r--src/js/messaging.js99
-rw-r--r--src/js/pagestore.js44
-rw-r--r--src/js/popup-fenix.js69
-rw-r--r--src/js/redirect-engine.js45
-rw-r--r--src/js/reverselookup.js2
-rw-r--r--src/js/s14e-serializer.js1405
-rw-r--r--src/js/scriptlet-filtering-core.js28
-rw-r--r--src/js/scriptlet-filtering.js202
-rw-r--r--src/js/scriptlets/epicker.js45
-rw-r--r--src/js/scriptlets/scriptlet-loglevel-1.js49
-rw-r--r--src/js/scriptlets/scriptlet-loglevel-2.js49
-rw-r--r--src/js/scriptlets/should-inject-contentscript.js2
-rw-r--r--src/js/settings.js53
-rw-r--r--src/js/start.js183
-rw-r--r--src/js/static-dnr-filtering.js6
-rw-r--r--src/js/static-ext-filtering-db.js12
-rw-r--r--src/js/static-ext-filtering.js45
-rw-r--r--src/js/static-filtering-parser.js52
-rw-r--r--src/js/static-net-filtering.js299
-rw-r--r--src/js/storage.js355
-rw-r--r--src/js/traffic.js24
-rw-r--r--src/js/ublock.js2
-rw-r--r--src/js/whitelist.js52
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>&ensp;' +
- '<input type="search" spellcheck="false">&emsp;' +
- '<span class="cm-search-widget-up cm-search-widget-button fa-icon">angle-up</span>&nbsp;' +
- '<span class="cm-search-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span>&emsp;' +
- '<span class="cm-search-widget-count"></span>' +
- '</span>' +
- '<span class="cm-linter-widget" data-lint="0">' +
- '<span class="cm-linter-widget-count"></span>&emsp;' +
- '<span class="cm-linter-widget-up cm-search-widget-button fa-icon">angle-up</span>&nbsp;' +
- '<span class="cm-linter-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span>&emsp;' +
- '</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>&ensp;',
+ '<span class="cm-search-widget-input">',
+ '<span class="searchfield">',
+ '<input type="search" spellcheck="false" placeholder="">',
+ '<span class="fa-icon">search</span>',
+ '</span>&ensp;',
+ '<span class="cm-search-widget-up cm-search-widget-button fa-icon">angle-up</span>&nbsp;',
+ '<span class="cm-search-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span>&ensp;',
+ '<span class="cm-search-widget-count"></span>',
+ '</span>',
+ '<span class="cm-linter-widget" data-lint="0">',
+ '<span class="cm-linter-widget-count"></span>&ensp;',
+ '<span class="cm-linter-widget-up cm-search-widget-button fa-icon">angle-up</span>&nbsp;',
+ '<span class="cm-linter-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span>&ensp;',
+ '</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;
};